diff --git a/.gitignore b/.gitignore index c97ba8da9f1..84b8e831993 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,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 7f00e63f2e0..1cddfa323d6 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 0927595a2c2..a08041e9fd4 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 291181c77f5..00000000000 --- 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 00000000000..73f38ab77f8 --- /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 00000000000..d9ad0303955 --- /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 2a76d5184c4..193a8124d98 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/debug/unit_tests/tests/nfc/nfc_test.c b/applications/debug/unit_tests/tests/nfc/nfc_test.c index 0898ac8edac..4ba934b6d55 100644 --- a/applications/debug/unit_tests/tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/tests/nfc/nfc_test.c @@ -496,7 +496,7 @@ NfcCommand mf_classic_poller_send_frame_callback(NfcGenericEventEx event, void* MfClassicKey key = { .data = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, }; - error = mf_classic_poller_auth(instance, 0, &key, MfClassicKeyTypeA, NULL); + error = mf_classic_poller_auth(instance, 0, &key, MfClassicKeyTypeA, NULL, false); frame_test->state = (error == MfClassicErrorNone) ? NfcTestMfClassicSendFrameTestStateReadBlock : NfcTestMfClassicSendFrameTestStateFail; diff --git a/applications/examples/example_event_loop/application.fam b/applications/examples/example_event_loop/application.fam index a37ffb1a041..15a7c8837f8 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 00000000000..5d0acf7f10f --- /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 ebfb0091183..ae748da55fb 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 d043f3f8990..20bf7af4b09 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 65dbd83cf55..6f72809739f 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/application.fam b/applications/main/nfc/application.fam index a11e09d6b3d..6dbde7c3718 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -218,6 +218,35 @@ App( sources=["plugins/supported_cards/trt.c"], ) +App( + appid="ndef_ul_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_UL")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) +App( + appid="ndef_mfc_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_MFC")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + +App( + appid="ndef_slix_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_SLIX")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + App( appid="nfc_start", targets=["f7"], diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 295a75a4e7f..14e484622c0 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -74,8 +74,12 @@ #define NFC_APP_MFKEY32_LOGS_FILE_NAME ".mfkey32.log" #define NFC_APP_MFKEY32_LOGS_FILE_PATH (NFC_APP_FOLDER "/" NFC_APP_MFKEY32_LOGS_FILE_NAME) -#define NFC_APP_MF_CLASSIC_DICT_USER_PATH (NFC_APP_FOLDER "/assets/mf_classic_dict_user.nfc") +#define NFC_APP_MF_CLASSIC_DICT_USER_PATH (NFC_APP_FOLDER "/assets/mf_classic_dict_user.nfc") +#define NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH \ + (NFC_APP_FOLDER "/assets/mf_classic_dict_user_nested.nfc") #define NFC_APP_MF_CLASSIC_DICT_SYSTEM_PATH (NFC_APP_FOLDER "/assets/mf_classic_dict.nfc") +#define NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH \ + (NFC_APP_FOLDER "/assets/mf_classic_dict_nested.nfc") typedef enum { NfcRpcStateIdle, @@ -93,6 +97,12 @@ typedef struct { bool is_key_attack; uint8_t key_attack_current_sector; bool is_card_present; + MfClassicNestedPhase nested_phase; + MfClassicPrngType prng_type; + MfClassicBackdoor backdoor; + uint16_t nested_target_key; + uint16_t msb_count; + bool enhanced_dict; } NfcMfClassicDictAttackContext; struct NfcApp { diff --git a/applications/main/nfc/plugins/supported_cards/clipper.c b/applications/main/nfc/plugins/supported_cards/clipper.c index 35d0c70390d..3eba82425d1 100644 --- a/applications/main/nfc/plugins/supported_cards/clipper.c +++ b/applications/main/nfc/plugins/supported_cards/clipper.c @@ -139,6 +139,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. // @@ -149,6 +162,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 new file mode 100644 index 00000000000..fb2c4da480a --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -0,0 +1,1065 @@ +// Parser for NDEF format data +// Supports multiple NDEF messages and records in same tag +// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty, SmartPoster +// Documentation and sources indicated where relevant +// Made by @Willy-JL +// Mifare Ultralight support by @Willy-JL +// Mifare Classic support by @luu176 & @Willy-JL +// SLIX support by @Willy-JL + +// We use an arbitrary position system here, in order to support more protocols. +// Each protocol parses basic structure of the card, then starts ndef_parse_tlv() +// using an arbitrary position value that it can understand. When accessing data +// to parse NDEF content, ndef_get() will then map this arbitrary value to the +// card using state in Ndef struct, skipping blocks or sectors as needed. This +// way, NDEF parsing code does not need to know details of card layout. + +#include "nfc_supported_card_plugin.h" +#include + +#include +#include +#include + +#include + +#define TAG "NDEF" + +#define NDEF_PROTO_INVALID (-1) +#define NDEF_PROTO_RAW (0) // For parsing data fed manually +#define NDEF_PROTO_UL (1) +#define NDEF_PROTO_MFC (2) +#define NDEF_PROTO_SLIX (3) +#define NDEF_PROTO_TOTAL (4) + +#ifndef NDEF_PROTO +#error Must specify what protocol to use with NDEF_PROTO define! +#endif +#if NDEF_PROTO <= NDEF_PROTO_INVALID || NDEF_PROTO >= NDEF_PROTO_TOTAL +#error Invalid NDEF_PROTO specified! +#endif + +#define NDEF_TITLE(device, parsed_data) \ + furi_string_printf( \ + parsed_data, \ + "\e#NDEF Format Data\nCard type: %s\n", \ + nfc_device_get_name(device, NfcDeviceNameTypeFull)) + +// ---=== structures ===--- + +// TLV structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data +typedef enum FURI_PACKED { + NdefTlvPadding = 0x00, + NdefTlvLockControl = 0x01, + NdefTlvMemoryControl = 0x02, + NdefTlvNdefMessage = 0x03, + NdefTlvProprietary = 0xFD, + NdefTlvTerminator = 0xFE, +} NdefTlv; + +// Type Name Format values: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf +typedef enum FURI_PACKED { + NdefTnfEmpty = 0x00, + NdefTnfWellKnownType = 0x01, + NdefTnfMediaType = 0x02, + NdefTnfAbsoluteUri = 0x03, + NdefTnfExternalType = 0x04, + NdefTnfUnknown = 0x05, + NdefTnfUnchanged = 0x06, + NdefTnfReserved = 0x07, +} NdefTnf; + +// Flags and TNF structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf +typedef struct FURI_PACKED { + // Reversed due to endianness + NdefTnf type_name_format : 3; + bool id_length_present : 1; + bool short_record : 1; + bool chunk_flag : 1; + bool message_end : 1; + bool message_begin : 1; +} NdefFlagsTnf; +_Static_assert(sizeof(NdefFlagsTnf) == 1); + +// URI payload format: +// https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 +static const char* ndef_uri_prepends[] = { + [0x00] = NULL, // Allows detecting no prepend and checking schema for type + [0x01] = "http://www.", + [0x02] = "https://www.", + [0x03] = "http://", + [0x04] = "https://", + [0x05] = "tel:", + [0x06] = "mailto:", + [0x07] = "ftp://anonymous:anonymous@", + [0x08] = "ftp://ftp.", + [0x09] = "ftps://", + [0x0A] = "sftp://", + [0x0B] = "smb://", + [0x0C] = "nfs://", + [0x0D] = "ftp://", + [0x0E] = "dav://", + [0x0F] = "news:", + [0x10] = "telnet://", + [0x11] = "imap:", + [0x12] = "rtsp://", + [0x13] = "urn:", + [0x14] = "pop:", + [0x15] = "sip:", + [0x16] = "sips:", + [0x17] = "tftp:", + [0x18] = "btspp://", + [0x19] = "btl2cap://", + [0x1A] = "btgoep://", + [0x1B] = "tcpobex://", + [0x1C] = "irdaobex://", + [0x1D] = "file://", + [0x1E] = "urn:epc:id:", + [0x1F] = "urn:epc:tag:", + [0x20] = "urn:epc:pat:", + [0x21] = "urn:epc:raw:", + [0x22] = "urn:epc:", + [0x23] = "urn:nfc:", +}; + +// ---=== card memory layout abstraction ===--- + +// Shared context and state, read above +typedef struct { + FuriString* output; +#if NDEF_PROTO == NDEF_PROTO_RAW + struct { + const uint8_t* data; + size_t size; + } raw; +#elif NDEF_PROTO == NDEF_PROTO_UL + struct { + const uint8_t* start; + size_t size; + } ul; +#elif NDEF_PROTO == NDEF_PROTO_MFC + struct { + const MfClassicBlock* blocks; + size_t size; + } mfc; +#elif NDEF_PROTO == NDEF_PROTO_SLIX + struct { + const uint8_t* start; + size_t size; + } slix; +#endif +} Ndef; + +static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { +#if NDEF_PROTO == NDEF_PROTO_RAW + + // Using user-provided pointer, simply need to remap to it + if(pos + len > ndef->raw.size) return false; + memcpy(buf, ndef->raw.data + pos, len); + return true; + +#elif NDEF_PROTO == NDEF_PROTO_UL + + // Memory space is contiguous, simply need to remap to data pointer + if(pos + len > ndef->ul.size) return false; + memcpy(buf, ndef->ul.start + pos, len); + return true; + +#elif NDEF_PROTO == NDEF_PROTO_MFC + + // We need to skip sector trailers and MAD2, NDEF parsing just uses + // a position offset in data space, as if it were contiguous. + + // Start with a simple data space size check + if(pos + len > ndef->mfc.size) return false; + + // First 128 blocks are 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 93 (31*3) data blocks correspond to 128 real blocks. + // Last 128 blocks are 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 120 (8*15) data blocks correspond to 128 real blocks. + div_t small_sector_data_blocks = div(pos, MF_CLASSIC_BLOCK_SIZE); + size_t large_sector_data_blocks = 0; + if(small_sector_data_blocks.quot > 93) { + large_sector_data_blocks = small_sector_data_blocks.quot - 93; + small_sector_data_blocks.quot = 93; + } + + div_t small_sectors = div(small_sector_data_blocks.quot, 3); + size_t real_block = small_sectors.quot * 4 + small_sectors.rem; + if(small_sectors.quot >= 16) { + real_block += 4; // Skip MAD2 + } + if(large_sector_data_blocks) { + div_t large_sectors = div(large_sector_data_blocks, 15); + real_block += large_sectors.quot * 16 + large_sectors.rem; + } + + const uint8_t* cur = &ndef->mfc.blocks[real_block].data[small_sector_data_blocks.rem]; + while(len) { + size_t sector_trailer = mf_classic_get_sector_trailer_num_by_block(real_block); + const uint8_t* end = &ndef->mfc.blocks[sector_trailer].data[0]; + + size_t chunk_len = MIN((size_t)(end - cur), len); + memcpy(buf, cur, chunk_len); + len -= chunk_len; + + if(len) { + real_block = sector_trailer + 1; + if(real_block == 64) { + real_block += 4; // Skip MAD2 + } + cur = &ndef->mfc.blocks[real_block].data[0]; + } + } + + return true; + +#elif NDEF_PROTO == NDEF_PROTO_SLIX + + // Memory space is contiguous, simply need to remap to data pointer + if(pos + len > ndef->slix.size) return false; + memcpy(buf, ndef->slix.start + pos, len); + return true; + +#else + + UNUSED(ndef); + UNUSED(pos); + UNUSED(len); + UNUSED(buf); + return false; + +#endif +} + +// ---=== output helpers ===--- + +static inline bool is_printable(char c) { + return (c >= ' ' && c <= '~') || c == '\r' || c == '\n'; +} + +static bool is_text(const uint8_t* buf, size_t len) { + for(size_t i = 0; i < len; i++) { + if(!is_printable(buf[i])) return false; + } + return true; +} + +static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + // We don't have direct access to memory chunks due to different card layouts + // Making a temporary buffer is wasteful of RAM and we can't afford this + // So while iterating like this is inefficient, it saves RAM and works between multiple card types + if(!force_hex) { + // If we find a non-printable character along the way, reset string to prev state and re-do as hex + size_t string_prev = furi_string_size(ndef->output); + for(size_t i = 0; i < len; i++) { + char c; + if(!ndef_get(ndef, pos + i, 1, &c)) return false; + if(!is_printable(c)) { + furi_string_left(ndef->output, string_prev); + force_hex = true; + break; + } + furi_string_push_back(ndef->output, c); + } + } + if(force_hex) { + for(size_t i = 0; i < len; i++) { + uint8_t b; + if(!ndef_get(ndef, pos + i, 1, &b)) return false; + furi_string_cat_printf(ndef->output, "%02X ", b); + } + } + furi_string_cat(ndef->output, "\n"); + return true; +} + +static void + ndef_print(Ndef* ndef, const char* prefix, const void* buf, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + if(!force_hex && is_text(buf, len)) { + furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); + } else { + for(size_t i = 0; i < len; i++) { + furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); + } + } + furi_string_cat(ndef->output, "\n"); +} + +// ---=== payload parsing ===--- + +static inline uint8_t hex_to_int(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + return 0; +} + +static char url_decode_char(const char* str) { + return (hex_to_int(str[0]) << 4) | hex_to_int(str[1]); +} + +static bool ndef_parse_uri(Ndef* ndef, size_t pos, size_t len) { + const char* type = "URI"; + + // Parse URI prepend type + const char* prepend = NULL; + uint8_t prepend_type; + if(!ndef_get(ndef, pos++, 1, &prepend_type)) return false; + len--; + if(prepend_type < COUNT_OF(ndef_uri_prepends)) { + prepend = ndef_uri_prepends[prepend_type]; + } + if(prepend) { + if(strncmp(prepend, "http", 4) == 0) { + type = "URL"; + } else if(strncmp(prepend, "tel:", 4) == 0) { + type = "Phone"; + prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output + } else if(strncmp(prepend, "mailto:", 7) == 0) { + type = "Mail"; + prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output + } + } + + // Parse and optionally skip schema, if no prepend was specified + if(!prepend) { + char schema[7] = {0}; // Longest schema we check is 7 char long without terminator + if(!ndef_get(ndef, pos, MIN(sizeof(schema), len), schema)) return false; + if(strncmp(schema, "http", 4) == 0) { + type = "URL"; + } else if(strncmp(schema, "tel:", 4) == 0) { + type = "Phone"; + pos += 4; + len -= 4; + } else if(strncmp(schema, "mailto:", 7) == 0) { + type = "Mail"; + pos += 7; + len -= 7; + } + } + + // Print static data as-is + furi_string_cat_printf(ndef->output, "%s\n", type); + if(prepend) { + furi_string_cat(ndef->output, prepend); + } + + // Print URI one char at a time and perform URL decode + while(len) { + char c; + if(!ndef_get(ndef, pos++, 1, &c)) return false; + len--; + if(c != '%' || len < 2) { // Not encoded, or not enough remaining text for encoded char + furi_string_push_back(ndef->output, c); + continue; + } + char enc[2]; + if(!ndef_get(ndef, pos, 2, enc)) return false; + enc[0] = toupper(enc[0]); + enc[1] = toupper(enc[1]); + // Only consume and print these 2 characters if they're valid URL encoded + // Otherwise they're processed in next iterations and we output the % char + if(((enc[0] >= 'A' && enc[0] <= 'F') || (enc[0] >= '0' && enc[0] <= '9')) && + ((enc[1] >= 'A' && enc[1] <= 'F') || (enc[1] >= '0' && enc[1] <= '9'))) { + pos += 2; + len -= 2; + c = url_decode_char(enc); + } + furi_string_push_back(ndef->output, c); + } + + return true; +} + +static bool ndef_parse_text(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "Text\n"); + if(!ndef_dump(ndef, NULL, pos + 3, len - 3, false)) return false; + return true; +} + +static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "BT MAC\n"); + if(len != 8) return false; + if(!ndef_dump(ndef, NULL, pos + 2, len - 2, true)) return false; + return true; +} + +static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { + // We hide redundant tags the user is probably not interested in. + // Would be easier with FuriString checks for start_with() and end_with() + // but to do that would waste lots of RAM on a cloned string buffer + // so instead we just look for these markers at start/end and shift + // pos and len then use ndef_dump() to output one char at a time. + // Results in minimal stack and no heap usage at all. + static const char* const begin_tag = "BEGIN:VCARD"; + static const uint8_t begin_len = strlen(begin_tag); + static const char* const version_tag = "VERSION:"; + static const uint8_t version_len = strlen(version_tag); + static const char* const end_tag = "END:VCARD"; + static const uint8_t end_len = strlen(end_tag); + char tmp[13] = {0}; // Enough for BEGIN:VCARD\r\n + uint8_t skip = 0; + + // Skip BEGIN tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(begin_tag, tmp, begin_len) == 0) { + skip = begin_len; + if(tmp[skip] == '\r') skip++; + if(tmp[skip] == '\n') skip++; + pos += skip; + len -= skip; + } + } + + // Skip VERSION tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(version_tag, tmp, version_len) == 0) { + skip = version_len; + while(skip < len) { + if(!ndef_get(ndef, pos + skip, 1, &tmp[0])) return false; + skip++; + if(tmp[0] == '\n') break; + } + pos += skip; + len -= skip; + } + } + + // Skip END tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos + len - sizeof(tmp), sizeof(tmp), tmp)) return false; + // Read more than length of END tag and check multiple offsets, might have some padding after + // Worst case: there is END:VCARD\r\n\r\n which is same length as tmp buffer (13) + // Not sure if this is in spec but might aswell check + static const uint8_t offsets = sizeof(tmp) - end_len + 1; + for(uint8_t offset = 0; offset < offsets; offset++) { + if(strncmp(end_tag, tmp + offset, end_len) == 0) { + skip = sizeof(tmp) - offset; + len -= skip; + break; + } + } + } + + furi_string_cat(ndef->output, "Contact\n"); + ndef_dump(ndef, NULL, pos, len, false); + + return true; +} + +// Loosely based on Android WiFi NDEF implementation: +// https://android.googlesource.com/platform/packages/apps/Nfc/+/025560080737b43876c9d81feff3151f497947e8/src/com/android/nfc/NfcWifiProtectedSetup.java +static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { +#define CREDENTIAL_FIELD_ID (0x100E) +#define SSID_FIELD_ID (0x1045) +#define NETWORK_KEY_FIELD_ID (0x1027) +#define AUTH_TYPE_FIELD_ID (0x1003) +#define AUTH_TYPE_EXPECTED_SIZE (2) +#define AUTH_TYPE_OPEN (0x0001) +#define AUTH_TYPE_WPA_PSK (0x0002) +#define AUTH_TYPE_WPA_EAP (0x0008) +#define AUTH_TYPE_WPA2_EAP (0x0010) +#define AUTH_TYPE_WPA2_PSK (0x0020) +#define AUTH_TYPE_WPA_AND_WPA2_PSK (0x0022) +#define MAX_NETWORK_KEY_SIZE_BYTES (64) + + furi_string_cat(ndef->output, "WiFi\n"); + size_t end = pos + len; + + uint8_t tmp_buf[2]; + while(pos < end) { + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t field_id = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t field_len = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + FURI_LOG_D(TAG, "wifi field: %04X len: %d", field_id, field_len); + + if(pos + field_len > end) { + return false; + } + + if(field_id == CREDENTIAL_FIELD_ID) { + size_t field_end = pos + field_len; + while(pos < field_end) { + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t cfg_id = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t cfg_len = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + FURI_LOG_D(TAG, "wifi cfg: %04X len: %d", cfg_id, cfg_len); + + if(pos + cfg_len > field_end) { + return false; + } + + switch(cfg_id) { + case SSID_FIELD_ID: + if(!ndef_dump(ndef, "SSID", pos, cfg_len, false)) return false; + pos += cfg_len; + break; + case NETWORK_KEY_FIELD_ID: + if(cfg_len > MAX_NETWORK_KEY_SIZE_BYTES) { + return false; + } + if(!ndef_dump(ndef, "PWD", pos, cfg_len, false)) return false; + pos += cfg_len; + break; + case AUTH_TYPE_FIELD_ID: + if(cfg_len != AUTH_TYPE_EXPECTED_SIZE) { + return false; + } + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t auth_type = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + const char* auth; + switch(auth_type) { + case AUTH_TYPE_OPEN: + auth = "Open"; + break; + case AUTH_TYPE_WPA_PSK: + auth = "WPA Personal"; + break; + case AUTH_TYPE_WPA_EAP: + auth = "WPA Enterprise"; + break; + case AUTH_TYPE_WPA2_EAP: + auth = "WPA2 Enterprise"; + break; + case AUTH_TYPE_WPA2_PSK: + auth = "WPA2 Personal"; + break; + case AUTH_TYPE_WPA_AND_WPA2_PSK: + auth = "WPA/WPA2 Personal"; + break; + default: + auth = "Unknown"; + break; + } + ndef_print(ndef, "AUTH", auth, strlen(auth), false); + break; + default: + pos += cfg_len; + break; + } + } + return true; + } + pos += field_len; + } + + furi_string_cat(ndef->output, "No data parsed\n"); + return true; +} + +// ---=== ndef layout parsing ===--- + +bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, + const char* type, + uint8_t type_len); +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed); + +bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, + const char* type, + uint8_t 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; + } + + switch(tnf) { + case NdefTnfWellKnownType: + if(strncmp("Sp", type, type_len) == 0) { + furi_string_cat(ndef->output, "SmartPoster\nContained records below\n\n"); + return ndef_parse_message(ndef, pos, len, 0, true); + } else if(strncmp("U", type, type_len) == 0) { + return ndef_parse_uri(ndef, pos, len); + } else if(strncmp("T", type, type_len) == 0) { + return ndef_parse_text(ndef, pos, len); + } + // Dump data without parsing + furi_string_cat(ndef->output, "Unknown\n"); + ndef_print(ndef, "Well-known Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfMediaType: + if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { + return ndef_parse_bt(ndef, pos, len); + } else if(strncmp("text/vcard", type, type_len) == 0) { + return ndef_parse_vcard(ndef, pos, len); + } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { + return ndef_parse_wifi(ndef, pos, len); + } + // Dump data without parsing + furi_string_cat(ndef->output, "Unknown\n"); + ndef_print(ndef, "Media Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfEmpty: + case NdefTnfAbsoluteUri: + case NdefTnfExternalType: + case NdefTnfUnknown: + case NdefTnfUnchanged: + case NdefTnfReserved: + default: + // Dump data without parsing + furi_string_cat(ndef->output, "Unsupported\n"); + ndef_print(ndef, "Type name format", &tnf, 1, true); + ndef_print(ndef, "Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + } +} + +// NDEF message structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { + size_t end = pos + len; + + size_t record_num = 0; + bool last_record = false; + while(pos < end) { + // Flags and TNF + NdefFlagsTnf flags_tnf; + if(!ndef_get(ndef, pos++, 1, &flags_tnf)) return false; + FURI_LOG_D(TAG, "flags_tnf: %02X", *(uint8_t*)&flags_tnf); + FURI_LOG_D(TAG, "flags_tnf.message_begin: %d", flags_tnf.message_begin); + FURI_LOG_D(TAG, "flags_tnf.message_end: %d", flags_tnf.message_end); + FURI_LOG_D(TAG, "flags_tnf.chunk_flag: %d", flags_tnf.chunk_flag); + FURI_LOG_D(TAG, "flags_tnf.short_record: %d", flags_tnf.short_record); + FURI_LOG_D(TAG, "flags_tnf.id_length_present: %d", flags_tnf.id_length_present); + FURI_LOG_D(TAG, "flags_tnf.type_name_format: %02X", flags_tnf.type_name_format); + // Message Begin should only be set on first record + if(record_num++ && flags_tnf.message_begin) return false; + // Message End should only be set on last record + if(last_record) return false; + if(flags_tnf.message_end) last_record = true; + // Chunk Flag not supported + if(flags_tnf.chunk_flag) return false; + + // Type Length + uint8_t type_len; + if(!ndef_get(ndef, pos++, 1, &type_len)) return false; + + // Payload Length field of 1 or 4 bytes + uint32_t payload_len; + if(flags_tnf.short_record) { + uint8_t payload_len_short; + if(!ndef_get(ndef, pos++, 1, &payload_len_short)) return false; + payload_len = payload_len_short; + } else { + if(!ndef_get(ndef, pos, sizeof(payload_len), &payload_len)) return false; + payload_len = bit_lib_bytes_to_num_be((void*)&payload_len, sizeof(payload_len)); + pos += sizeof(payload_len); + } + + // ID Length + uint8_t id_len = 0; + if(flags_tnf.id_length_present) { + if(!ndef_get(ndef, pos++, 1, &id_len)) return false; + } + + // Payload Type + char type_buf[32]; // Longest type supported in ndef_parse_record() is 32 chars excl terminator + char* type = type_buf; + bool type_was_allocated = false; + if(type_len) { + if(type_len > sizeof(type_buf)) { + type = malloc(type_len); + type_was_allocated = true; + } + if(!ndef_get(ndef, pos, type_len, type)) { + if(type_was_allocated) free(type); + return false; + } + pos += type_len; + } + + // Payload ID + pos += id_len; + + if(smart_poster) { + furi_string_cat_printf(ndef->output, "\e*> SP-R%zu: ", record_num); + } else { + 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); + return false; + } + pos += payload_len; + + if(type_was_allocated) free(type); + furi_string_trim(ndef->output, "\n"); + furi_string_cat(ndef->output, "\n\n"); + } + + if(record_num == 0) { + if(smart_poster) { + furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%zu: Empty\n\n", message_num); + } + } + + return pos == end && (last_record || record_num == 0); +} + +// TLV structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed) { + size_t end = pos + len; + size_t message_num = 0; + + while(pos < end) { + NdefTlv tlv; + if(!ndef_get(ndef, pos++, 1, &tlv)) return 0; + FURI_LOG_D(TAG, "tlv: %02X", tlv); + + switch(tlv) { + default: + // Unknown, bail to avoid problems + return 0; + + case NdefTlvPadding: + // Has no length, skip to next byte + break; + + case NdefTlvTerminator: + // NDEF message finished, return whether we parsed something + return message_num; + + case NdefTlvLockControl: + case NdefTlvMemoryControl: + case NdefTlvProprietary: + case NdefTlvNdefMessage: { + // Calculate length + uint16_t len; + uint8_t len_type; + if(!ndef_get(ndef, pos++, 1, &len_type)) return 0; + if(len_type < 0xFF) { // 1 byte length + len = len_type; + } else { // 3 byte length (0xFF marker + 2 byte integer) + if(!ndef_get(ndef, pos, sizeof(len), &len)) return 0; + len = bit_lib_bytes_to_num_be((void*)&len, sizeof(len)); + pos += sizeof(len); + } + + if(tlv != NdefTlvNdefMessage) { + // We don't care, skip this TLV block to next one + pos += len; + break; + } + + if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed, false)) + return 0; + pos += len; + + break; + } + } + } + + // Reached data end with no TLV terminator, + // but also no errors, treat this as a success + return message_num; +} + +#if NDEF_PROTO != NDEF_PROTO_RAW + +// ---=== protocol entry-points ===--- + +#if NDEF_PROTO == NDEF_PROTO_UL + +// MF UL memory layout: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#memory_layout +static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight); + + // Check card type can contain NDEF + if(data->type != MfUltralightTypeNTAG203 && data->type != MfUltralightTypeNTAG213 && + data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && + data->type != MfUltralightTypeNTAGI2C1K && data->type != MfUltralightTypeNTAGI2C2K && + data->type != MfUltralightTypeNTAGI2CPlus1K && + data->type != MfUltralightTypeNTAGI2CPlus2K) { + return false; + } + + // Check Capability Container (CC) values + struct { + uint8_t nfc_magic_number; + uint8_t document_version_number; + uint8_t data_area_size; // Usable byte size / 8, only includes user memory + uint8_t read_write_access; + }* cc = (void*)&data->page[3].data[0]; + if(cc->nfc_magic_number != 0xE1) return false; + if(cc->document_version_number != 0x10) return false; + + // Calculate usable data area + const uint8_t* start = &data->page[4].data[0]; + const uint8_t* end = start + (cc->data_area_size * 8); + size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; + end = MIN(end, &data->page[0].data[0] + max_size); + size_t data_start = 0; + size_t data_size = end - start; + + NDEF_TITLE(device, parsed_data); + + Ndef ndef = { + .output = parsed_data, + .ul = + { + .start = start, + .size = data_size, + }, + }; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); + + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } + + return parsed > 0; +} + +#elif NDEF_PROTO == NDEF_PROTO_MFC + +// MFC MAD datasheet: +// https://www.nxp.com/docs/en/application-note/AN10787.pdf +#define AID_SIZE (2) +static const uint64_t mad_key = 0xA0A1A2A3A4A5; + +// NDEF on MFC breakdown: +// https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#storing-ndef-messages-in-mifare-sectors-607778 +static const uint8_t ndef_aid[AID_SIZE] = {0x03, 0xE1}; + +static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + // Check card type can contain NDEF + if(data->type != MfClassicType1k && data->type != MfClassicType4k && + data->type != MfClassicTypeMini) { + return false; + } + + // Check MADs for what sectors contain NDEF data AIDs + bool sectors_with_ndef[MF_CLASSIC_TOTAL_SECTORS_MAX] = {0}; + const size_t sector_count = mf_classic_get_total_sectors_num(data->type); + const struct { + size_t block; + uint8_t aid_count; + } mads[2] = { + {1, 15}, + {64, 23}, + }; + for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { + const size_t block = mads[mad].block; + const size_t sector = mf_classic_get_sector_by_block(block); + if(sector_count <= sector) break; // Skip this MAD if not present + // Check MAD key + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector); + const uint64_t sector_key_a = bit_lib_bytes_to_num_be( + sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); + if(sector_key_a != mad_key) return false; + // Find NDEF AIDs + for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { + const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; + if(memcmp(aid, ndef_aid, AID_SIZE) == 0) { + sectors_with_ndef[aid_index + 1] = true; + } + } + } + + NDEF_TITLE(device, parsed_data); + + // Calculate how large the data space is, so excluding sector trailers and MAD2. + // Makes sure we stay within this card's actual content when parsing. + // First 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 32 sectors correspond to 93 (31*3) data blocks. + // Last 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 8 sectors correspond to 120 (8*15) data blocks. + size_t data_size; + if(sector_count > 32) { + data_size = 93 + (sector_count - 32) * 15; + } else { + data_size = sector_count * 3; + if(sector_count >= 16) { + data_size -= 3; // Skip MAD2 + } + } + data_size *= MF_CLASSIC_BLOCK_SIZE; + + Ndef ndef = { + .output = parsed_data, + .mfc = + { + .blocks = data->block, + .size = data_size, + }, + }; + size_t total_parsed = 0; + + for(size_t sector = 0; sector < sector_count; sector++) { + if(!sectors_with_ndef[sector]) continue; + FURI_LOG_D(TAG, "sector: %d", sector); + size_t string_prev = furi_string_size(parsed_data); + + // Convert real sector number to data block number + // to skip sector trailers and MAD2 + size_t data_block; + if(sector < 32) { + data_block = sector * 3; + if(sector >= 16) { + data_block -= 3; // Skip MAD2 + } + } else { + data_block = 93 + (sector - 32) * 15; + } + 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); + + if(parsed) { + total_parsed += parsed; + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_left(parsed_data, string_prev); + } + } + + if(!total_parsed) { + furi_string_reset(parsed_data); + } + + return total_parsed > 0; +} + +#elif NDEF_PROTO == NDEF_PROTO_SLIX + +// SLIX NDEF memory layout: +// https://community.nxp.com/pwmxy87654/attachments/pwmxy87654/nfc/7583/1/EEOL_2011FEB16_EMS_RFD_AN_01.pdf +static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + const Iso15693_3Data* data = nfc_device_get_data(device, NfcProtocolIso15693_3); + const uint8_t block_size = iso15693_3_get_block_size(data); + const uint16_t block_count = iso15693_3_get_block_count(data); + const uint8_t* blocks = simple_array_cget_data(data->block_data); + + // 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) { + return false; + } + + // Check Capability Container (CC) values + struct { + uint8_t nfc_magic_number; + uint8_t read_write_access : 4; // Reversed due to endianness + uint8_t document_version_number : 4; + uint8_t data_area_size; // Total byte size / 8, includes block 0 + uint8_t mbread_ipread; + }* cc = (void*)&blocks[0 * block_size]; + if(cc->nfc_magic_number != 0xE1) return false; + if(cc->document_version_number != 0x4) return false; + + // Calculate usable data area + const uint8_t* start = &blocks[1 * block_size]; + const uint8_t* end = blocks + (cc->data_area_size * 8); + size_t max_size = block_count * block_size; + end = MIN(end, blocks + max_size); + size_t data_start = 0; + size_t data_size = end - start; + + NDEF_TITLE(device, parsed_data); + + Ndef ndef = { + .output = parsed_data, + .slix = + { + .start = start, + .size = data_size, + }, + }; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); + + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } + + return parsed > 0; +} + +#endif + +// ---=== boilerplate ===--- + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin ndef_plugin = { + .verify = NULL, + .read = NULL, +#if NDEF_PROTO == NDEF_PROTO_UL + .parse = ndef_ul_parse, + .protocol = NfcProtocolMfUltralight, +#elif NDEF_PROTO == NDEF_PROTO_MFC + .parse = ndef_mfc_parse, + .protocol = NfcProtocolMfClassic, +#elif NDEF_PROTO == NDEF_PROTO_SLIX + .parse = ndef_slix_parse, + .protocol = NfcProtocolSlix, +#endif +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &ndef_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* ndef_plugin_ep(void) { + return &ndef_plugin_descriptor; +} + +#endif diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index c38140de272..9f2491691be 100644 --- a/applications/main/nfc/plugins/supported_cards/plantain.c +++ b/applications/main/nfc/plugins/supported_cards/plantain.c @@ -201,8 +201,9 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) { static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); - + size_t uid_len = 0; const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); bool parsed = false; @@ -220,12 +221,30 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { if(key != cfg.keys[cfg.data_sector].a) break; furi_string_printf(parsed_data, "\e#Plantain card\n"); + + const uint8_t* temp_ptr = &uid[0]; + + // UID is read from last to first byte + uint8_t card_number_tmp[uid_len]; + + if(uid_len == 4) { + for(size_t i = 0; i < 4; i++) { + card_number_tmp[i] = temp_ptr[3 - i]; + } + } else if(uid_len == 7) { + for(size_t i = 0; i < 7; i++) { + card_number_tmp[i] = temp_ptr[6 - i]; + } + } else { + break; + } + //UID is converted to a card number uint64_t card_number = 0; - for(size_t i = 0; i < 7; i++) { - card_number = (card_number << 8) | data->block[0].data[6 - i]; + for(size_t i = 0; i < uid_len; i++) { + card_number = (card_number << 8) | card_number_tmp[i]; } - // Print card number with 4-digit groups + // Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards. furi_string_cat_printf(parsed_data, "Number: "); FuriString* card_number_s = furi_string_alloc(); furi_string_cat_printf(card_number_s, "%llu", card_number); @@ -237,6 +256,7 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { furi_string_push_back(tmp_s, ' '); } furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s)); + // this works for 2K Plantain if(data->type == MfClassicType1k) { //balance uint32_t balance = 0; @@ -290,20 +310,70 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { last_payment_date.year, last_payment_date.hour, last_payment_date.minute); - //payment summ + //payment amount. This needs to be investigated more, currently it shows incorrect amount on some cards. uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8]; furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100); furi_string_free(card_number_s); furi_string_free(tmp_s); + //This is for 4K Plantains. } else if(data->type == MfClassicType4k) { + //balance + uint32_t balance = 0; + for(uint8_t i = 0; i < 4; i++) { + balance = (balance << 8) | data->block[16].data[3 - i]; + } + furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100); + //trips - uint8_t trips_metro = data->block[36].data[0]; - uint8_t trips_ground = data->block[36].data[1]; + uint8_t trips_metro = data->block[21].data[0]; + uint8_t trips_ground = data->block[21].data[1]; furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground); + //trip time + uint32_t last_trip_timestamp = 0; + for(uint8_t i = 0; i < 3; i++) { + last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i]; + } + DateTime last_trip = {0}; + from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010); + furi_string_cat_printf( + parsed_data, + "Trip start: %02d.%02d.%04d %02d:%02d\n", + last_trip.day, + last_trip.month, + last_trip.year, + last_trip.hour, + last_trip.minute); + //validator + uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4]; + furi_string_cat_printf(parsed_data, "Validator: %d\n", validator); + //tariff + uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6]; + furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100); //trips in metro furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro); //trips on ground furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground); + //last payment + uint32_t last_payment_timestamp = 0; + for(uint8_t i = 0; i < 3; i++) { + last_payment_timestamp = (last_payment_timestamp << 8) | + data->block[18].data[4 - i]; + } + DateTime last_payment_date = {0}; + from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010); + furi_string_cat_printf( + parsed_data, + "Last pay: %02d.%02d.%04d %02d:%02d\n", + last_payment_date.day, + last_payment_date.month, + last_payment_date.year, + last_payment_date.hour, + last_payment_date.minute); + //payment amount + uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8]; + furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100); + furi_string_free(card_number_s); + furi_string_free(tmp_s); } parsed = true; } while(false); diff --git a/applications/main/nfc/plugins/supported_cards/trt.c b/applications/main/nfc/plugins/supported_cards/trt.c index 5407ab1f73c..793ad7f2d97 100644 --- a/applications/main/nfc/plugins/supported_cards/trt.c +++ b/applications/main/nfc/plugins/supported_cards/trt.c @@ -40,7 +40,10 @@ static bool trt_parse(const NfcDevice* device, FuriString* parsed_data) { const uint8_t* full_record_pointer = &data->page[FULL_SALE_TIME_STAMP_PAGE].data[0]; uint32_t latest_sale_record = bit_lib_get_bits_32(partial_record_pointer, 3, 20); uint32_t latest_sale_full_record = bit_lib_get_bits_32(full_record_pointer, 0, 27); - if(latest_sale_record != (latest_sale_full_record & 0xFFFFF)) break; + if(latest_sale_record != (latest_sale_full_record & 0xFFFFF)) + break; // check if the copy matches + if((latest_sale_record == 0) || (latest_sale_full_record == 0)) + break; // prevent false positive // Parse date // yyy yyyymmmm dddddhhh hhnnnnnn diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c index 328e39132fc..024bc5c1ea9 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c @@ -1,11 +1,15 @@ #include "../nfc_app_i.h" +#include #include -#include #define TAG "NfcMfClassicDictAttack" +// TODO FL-3926: Fix lag when leaving the dictionary attack view after Hardnested +// TODO FL-3926: Re-enters backdoor detection between user and system dictionary if no backdoor is found + typedef enum { + DictAttackStateCUIDDictInProgress, DictAttackStateUserDictInProgress, DictAttackStateSystemDictInProgress, } DictAttackState; @@ -29,7 +33,9 @@ NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context) } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) { const MfClassicData* mfc_data = nfc_device_get_data(instance->nfc_device, NfcProtocolMfClassic); - mfc_event->data->poller_mode.mode = MfClassicPollerModeDictAttack; + mfc_event->data->poller_mode.mode = (instance->nfc_dict_context.enhanced_dict) ? + MfClassicPollerModeDictAttackEnhanced : + MfClassicPollerModeDictAttackStandard; mfc_event->data->poller_mode.data = mfc_data; instance->nfc_dict_context.sectors_total = mf_classic_get_total_sectors_num(mfc_data->type); @@ -58,6 +64,11 @@ NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context) instance->nfc_dict_context.sectors_read = data_update->sectors_read; instance->nfc_dict_context.keys_found = data_update->keys_found; instance->nfc_dict_context.current_sector = data_update->current_sector; + instance->nfc_dict_context.nested_phase = data_update->nested_phase; + instance->nfc_dict_context.prng_type = data_update->prng_type; + instance->nfc_dict_context.backdoor = data_update->backdoor; + instance->nfc_dict_context.nested_target_key = data_update->nested_target_key; + instance->nfc_dict_context.msb_count = data_update->msb_count; view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventDictAttackDataUpdate); } else if(mfc_event->type == MfClassicPollerEventTypeNextSector) { @@ -117,19 +128,75 @@ static void nfc_scene_mf_classic_dict_attack_update_view(NfcApp* instance) { dict_attack_set_keys_found(instance->dict_attack, mfc_dict->keys_found); dict_attack_set_current_dict_key(instance->dict_attack, mfc_dict->dict_keys_current); dict_attack_set_current_sector(instance->dict_attack, mfc_dict->current_sector); + dict_attack_set_nested_phase(instance->dict_attack, mfc_dict->nested_phase); + dict_attack_set_prng_type(instance->dict_attack, mfc_dict->prng_type); + dict_attack_set_backdoor(instance->dict_attack, mfc_dict->backdoor); + dict_attack_set_nested_target_key(instance->dict_attack, mfc_dict->nested_target_key); + dict_attack_set_msb_count(instance->dict_attack, mfc_dict->msb_count); } } static void nfc_scene_mf_classic_dict_attack_prepare_view(NfcApp* instance) { uint32_t state = scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfClassicDictAttack); + if(state == DictAttackStateCUIDDictInProgress) { + size_t cuid_len = 0; + const uint8_t* cuid = nfc_device_get_uid(instance->nfc_device, &cuid_len); + FuriString* cuid_dict_path = furi_string_alloc_printf( + "%s/mf_classic_dict_%08lx.nfc", + EXT_PATH("nfc/assets"), + (uint32_t)bit_lib_bytes_to_num_be(cuid + (cuid_len - 4), 4)); + + do { + if(!keys_dict_check_presence(furi_string_get_cstr(cuid_dict_path))) { + state = DictAttackStateUserDictInProgress; + break; + } + + instance->nfc_dict_context.dict = keys_dict_alloc( + furi_string_get_cstr(cuid_dict_path), + KeysDictModeOpenExisting, + sizeof(MfClassicKey)); + + if(keys_dict_get_total_keys(instance->nfc_dict_context.dict) == 0) { + keys_dict_free(instance->nfc_dict_context.dict); + state = DictAttackStateUserDictInProgress; + break; + } + + dict_attack_set_header(instance->dict_attack, "MF Classic CUID Dictionary"); + } while(false); + + furi_string_free(cuid_dict_path); + } if(state == DictAttackStateUserDictInProgress) { do { + instance->nfc_dict_context.enhanced_dict = true; + + if(keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH)) { + storage_common_remove( + instance->storage, NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH); + } + if(keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_SYSTEM_PATH)) { + storage_common_copy( + instance->storage, + NFC_APP_MF_CLASSIC_DICT_SYSTEM_PATH, + NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH); + } + if(!keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_USER_PATH)) { state = DictAttackStateSystemDictInProgress; break; } + if(keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH)) { + storage_common_remove(instance->storage, NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH); + } + storage_common_copy( + instance->storage, + NFC_APP_MF_CLASSIC_DICT_USER_PATH, + NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH); + instance->nfc_dict_context.dict = keys_dict_alloc( NFC_APP_MF_CLASSIC_DICT_USER_PATH, KeysDictModeOpenAlways, sizeof(MfClassicKey)); if(keys_dict_get_total_keys(instance->nfc_dict_context.dict) == 0) { @@ -164,7 +231,7 @@ void nfc_scene_mf_classic_dict_attack_on_enter(void* context) { NfcApp* instance = context; scene_manager_set_scene_state( - instance->scene_manager, NfcSceneMfClassicDictAttack, DictAttackStateUserDictInProgress); + instance->scene_manager, NfcSceneMfClassicDictAttack, DictAttackStateCUIDDictInProgress); nfc_scene_mf_classic_dict_attack_prepare_view(instance); dict_attack_set_card_state(instance->dict_attack, true); view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewDictAttack); @@ -193,7 +260,21 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfClassicDictAttack); if(event.type == SceneManagerEventTypeCustom) { if(event.event == NfcCustomEventDictAttackComplete) { - if(state == DictAttackStateUserDictInProgress) { + bool ran_nested_dict = instance->nfc_dict_context.nested_phase != + MfClassicNestedPhaseNone; + if(state == DictAttackStateCUIDDictInProgress) { + nfc_poller_stop(instance->poller); + nfc_poller_free(instance->poller); + keys_dict_free(instance->nfc_dict_context.dict); + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneMfClassicDictAttack, + DictAttackStateUserDictInProgress); + nfc_scene_mf_classic_dict_attack_prepare_view(instance); + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfClassic); + nfc_poller_start(instance->poller, nfc_dict_attack_worker_callback, instance); + consumed = true; + } else if(state == DictAttackStateUserDictInProgress && !(ran_nested_dict)) { nfc_poller_stop(instance->poller); nfc_poller_free(instance->poller); keys_dict_free(instance->nfc_dict_context.dict); @@ -222,7 +303,27 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent } else if(event.event == NfcCustomEventDictAttackSkip) { const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller); nfc_device_set_data(instance->nfc_device, NfcProtocolMfClassic, mfc_data); - if(state == DictAttackStateUserDictInProgress) { + bool ran_nested_dict = instance->nfc_dict_context.nested_phase != + MfClassicNestedPhaseNone; + if(state == DictAttackStateCUIDDictInProgress) { + if(instance->nfc_dict_context.is_card_present) { + nfc_poller_stop(instance->poller); + nfc_poller_free(instance->poller); + keys_dict_free(instance->nfc_dict_context.dict); + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneMfClassicDictAttack, + DictAttackStateUserDictInProgress); + nfc_scene_mf_classic_dict_attack_prepare_view(instance); + instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfClassic); + nfc_poller_start(instance->poller, nfc_dict_attack_worker_callback, instance); + } else { + nfc_scene_mf_classic_dict_attack_notify_read(instance); + scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); + dolphin_deed(DolphinDeedNfcReadSuccess); + } + consumed = true; + } else if(state == DictAttackStateUserDictInProgress && !(ran_nested_dict)) { if(instance->nfc_dict_context.is_card_present) { nfc_poller_stop(instance->poller); nfc_poller_free(instance->poller); @@ -240,7 +341,7 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent dolphin_deed(DolphinDeedNfcReadSuccess); } consumed = true; - } else if(state == DictAttackStateSystemDictInProgress) { + } else { nfc_scene_mf_classic_dict_attack_notify_read(instance); scene_manager_next_scene(instance->scene_manager, NfcSceneReadSuccess); dolphin_deed(DolphinDeedNfcReadSuccess); @@ -262,7 +363,7 @@ void nfc_scene_mf_classic_dict_attack_on_exit(void* context) { dict_attack_reset(instance->dict_attack); scene_manager_set_scene_state( - instance->scene_manager, NfcSceneMfClassicDictAttack, DictAttackStateUserDictInProgress); + instance->scene_manager, NfcSceneMfClassicDictAttack, DictAttackStateCUIDDictInProgress); keys_dict_free(instance->nfc_dict_context.dict); @@ -275,6 +376,20 @@ void nfc_scene_mf_classic_dict_attack_on_exit(void* context) { instance->nfc_dict_context.is_key_attack = false; instance->nfc_dict_context.key_attack_current_sector = 0; instance->nfc_dict_context.is_card_present = false; + instance->nfc_dict_context.nested_phase = MfClassicNestedPhaseNone; + instance->nfc_dict_context.prng_type = MfClassicPrngTypeUnknown; + instance->nfc_dict_context.backdoor = MfClassicBackdoorUnknown; + instance->nfc_dict_context.nested_target_key = 0; + instance->nfc_dict_context.msb_count = 0; + instance->nfc_dict_context.enhanced_dict = false; + + // Clean up temporary files used for nested dictionary attack + if(keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH)) { + storage_common_remove(instance->storage, NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH); + } + if(keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH)) { + storage_common_remove(instance->storage, NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH); + } nfc_blink_stop(instance); notification_message(instance->notifications, &sequence_display_backlight_enforce_auto); diff --git a/applications/main/nfc/views/dict_attack.c b/applications/main/nfc/views/dict_attack.c index 14298a6aa77..726076972f2 100644 --- a/applications/main/nfc/views/dict_attack.c +++ b/applications/main/nfc/views/dict_attack.c @@ -21,6 +21,11 @@ typedef struct { size_t dict_keys_current; bool is_key_attack; uint8_t key_attack_current_sector; + MfClassicNestedPhase nested_phase; + MfClassicPrngType prng_type; + MfClassicBackdoor backdoor; + uint16_t nested_target_key; + uint16_t msb_count; } DictAttackViewModel; static void dict_attack_draw_callback(Canvas* canvas, void* model) { @@ -34,9 +39,47 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { } else { char draw_str[32] = {}; canvas_set_font(canvas, FontSecondary); + + switch(m->nested_phase) { + case MfClassicNestedPhaseAnalyzePRNG: + furi_string_set(m->header, "PRNG Analysis"); + break; + case MfClassicNestedPhaseDictAttack: + case MfClassicNestedPhaseDictAttackVerify: + case MfClassicNestedPhaseDictAttackResume: + furi_string_set(m->header, "Nested Dictionary"); + break; + case MfClassicNestedPhaseCalibrate: + case MfClassicNestedPhaseRecalibrate: + furi_string_set(m->header, "Calibration"); + break; + case MfClassicNestedPhaseCollectNtEnc: + furi_string_set(m->header, "Nonce Collection"); + break; + default: + break; + } + + if(m->prng_type == MfClassicPrngTypeHard) { + furi_string_cat(m->header, " (Hard)"); + } + + if(m->backdoor != MfClassicBackdoorNone && m->backdoor != MfClassicBackdoorUnknown) { + if(m->nested_phase != MfClassicNestedPhaseNone) { + furi_string_cat(m->header, " (Backdoor)"); + } else { + furi_string_set(m->header, "Backdoor Read"); + } + } + canvas_draw_str_aligned( - canvas, 64, 0, AlignCenter, AlignTop, furi_string_get_cstr(m->header)); - if(m->is_key_attack) { + canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(m->header)); + if(m->nested_phase == MfClassicNestedPhaseCollectNtEnc) { + uint8_t nonce_sector = + m->nested_target_key / (m->prng_type == MfClassicPrngTypeWeak ? 4 : 2); + snprintf(draw_str, sizeof(draw_str), "Collecting from sector: %d", nonce_sector); + canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); + } else if(m->is_key_attack) { snprintf( draw_str, sizeof(draw_str), @@ -46,21 +89,48 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { snprintf(draw_str, sizeof(draw_str), "Unlocking sector: %d", m->current_sector); } canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str); - float dict_progress = m->dict_keys_total == 0 ? - 0 : - (float)(m->dict_keys_current) / (float)(m->dict_keys_total); - float progress = m->sectors_total == 0 ? 0 : - ((float)(m->current_sector) + dict_progress) / - (float)(m->sectors_total); - if(progress > 1.0f) { - progress = 1.0f; - } - if(m->dict_keys_current == 0) { - // Cause when people see 0 they think it's broken - snprintf(draw_str, sizeof(draw_str), "%d/%zu", 1, m->dict_keys_total); + float dict_progress = 0; + if(m->nested_phase == MfClassicNestedPhaseAnalyzePRNG || + m->nested_phase == MfClassicNestedPhaseDictAttack || + m->nested_phase == MfClassicNestedPhaseDictAttackVerify || + m->nested_phase == MfClassicNestedPhaseDictAttackResume) { + // Phase: Nested dictionary attack + uint8_t target_sector = + m->nested_target_key / (m->prng_type == MfClassicPrngTypeWeak ? 2 : 16); + dict_progress = (float)(target_sector) / (float)(m->sectors_total); + snprintf(draw_str, sizeof(draw_str), "%d/%d", target_sector, m->sectors_total); + } else if( + m->nested_phase == MfClassicNestedPhaseCalibrate || + m->nested_phase == MfClassicNestedPhaseRecalibrate || + m->nested_phase == MfClassicNestedPhaseCollectNtEnc) { + // Phase: Nonce collection + if(m->prng_type == MfClassicPrngTypeWeak) { + uint8_t target_sector = m->nested_target_key / 4; + dict_progress = (float)(target_sector) / (float)(m->sectors_total); + snprintf(draw_str, sizeof(draw_str), "%d/%d", target_sector, m->sectors_total); + } else { + uint16_t max_msb = UINT8_MAX + 1; + dict_progress = (float)(m->msb_count) / (float)(max_msb); + snprintf(draw_str, sizeof(draw_str), "%d/%d", m->msb_count, max_msb); + } } else { - snprintf( - draw_str, sizeof(draw_str), "%zu/%zu", m->dict_keys_current, m->dict_keys_total); + dict_progress = m->dict_keys_total == 0 ? + 0 : + (float)(m->dict_keys_current) / (float)(m->dict_keys_total); + if(m->dict_keys_current == 0) { + // Cause when people see 0 they think it's broken + snprintf(draw_str, sizeof(draw_str), "%d/%zu", 1, m->dict_keys_total); + } else { + snprintf( + draw_str, + sizeof(draw_str), + "%zu/%zu", + m->dict_keys_current, + m->dict_keys_total); + } + } + if(dict_progress > 1.0f) { + dict_progress = 1.0f; } elements_progress_bar_with_text(canvas, 0, 20, 128, dict_progress, draw_str); canvas_set_font(canvas, FontSecondary); @@ -132,6 +202,11 @@ void dict_attack_reset(DictAttack* instance) { model->dict_keys_total = 0; model->dict_keys_current = 0; model->is_key_attack = false; + model->nested_phase = MfClassicNestedPhaseNone; + model->prng_type = MfClassicPrngTypeUnknown; + model->backdoor = MfClassicBackdoorUnknown; + model->nested_target_key = 0; + model->msb_count = 0; furi_string_reset(model->header); }, false); @@ -242,3 +317,41 @@ void dict_attack_reset_key_attack(DictAttack* instance) { with_view_model( instance->view, DictAttackViewModel * model, { model->is_key_attack = false; }, true); } + +void dict_attack_set_nested_phase(DictAttack* instance, MfClassicNestedPhase nested_phase) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->nested_phase = nested_phase; }, true); +} + +void dict_attack_set_prng_type(DictAttack* instance, MfClassicPrngType prng_type) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->prng_type = prng_type; }, true); +} + +void dict_attack_set_backdoor(DictAttack* instance, MfClassicBackdoor backdoor) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->backdoor = backdoor; }, true); +} + +void dict_attack_set_nested_target_key(DictAttack* instance, uint16_t nested_target_key) { + furi_assert(instance); + + with_view_model( + instance->view, + DictAttackViewModel * model, + { model->nested_target_key = nested_target_key; }, + true); +} + +void dict_attack_set_msb_count(DictAttack* instance, uint16_t msb_count) { + furi_assert(instance); + + with_view_model( + instance->view, DictAttackViewModel * model, { model->msb_count = msb_count; }, true); +} diff --git a/applications/main/nfc/views/dict_attack.h b/applications/main/nfc/views/dict_attack.h index 30f3b3c44ae..b6c6fdbdc30 100644 --- a/applications/main/nfc/views/dict_attack.h +++ b/applications/main/nfc/views/dict_attack.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -45,6 +46,16 @@ void dict_attack_set_key_attack(DictAttack* instance, uint8_t sector); void dict_attack_reset_key_attack(DictAttack* instance); +void dict_attack_set_nested_phase(DictAttack* instance, MfClassicNestedPhase nested_phase); + +void dict_attack_set_prng_type(DictAttack* instance, MfClassicPrngType prng_type); + +void dict_attack_set_backdoor(DictAttack* instance, MfClassicBackdoor backdoor); + +void dict_attack_set_nested_target_key(DictAttack* instance, uint16_t target_key); + +void dict_attack_set_msb_count(DictAttack* instance, uint16_t msb_count); + #ifdef __cplusplus } #endif diff --git a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes index 886cf82121a..39d9b3e3c95 100644 --- a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes +++ b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes @@ -1,58 +1,59 @@ Filetype: Flipper SubGhz Keystore File Version: 0 Encryption: 1 -IV: 59 65 73 2C 20 61 6E 64 20 79 6F 75 20 74 6F 6F -25F6594BB3FA40C929F9CF43E5E6649A42047B727398F057589EAAC7430169E5 -B2D2A6E64C3A13406610C086DBF2D17F0C5643E44CD276D4F8933942B964DAA2 -76EE2549A4F62499533856346B3AAF535F248E23802111380D44B70014581E09 -482D1CD99F0F9940195777F482B6EF78B0895FDEAF721544A349B705FCFDF3DE -A83ED14173781D96C7434892D9A092A8658EDDC105FC35FFB1D1C727E0A9FD13F8C07A79F6778D858C0002265C8D49ED -3675A174F916BDCBE056B5C9C7B6461AC885969A140DAB923137F80CEBBBF64D -EE55B1DE8D6632FF91C090641032A290C15271E59C6B9E75328B7463F9029B9F -8A81B9B366DD0F00D1829B3DF62A3EF912B4F492E9D6E99BDC33519C53A9A24D -1C55C1195C5BFCC502448269211384EF13C979FABE39E4F3251E219F67C8DC4B -11F0FFEB6B9B308DFA9CFA0CD87F5D4EB2D0E48CC9D026C4FAE2C3D06EDE64E4 -11B3CC20AAD78DE62E835B922006635F6BA8D6D72088AEE51D6B0BA4DF8A87AB9A900F7B9D2AD87F0D9DC091EFFCE41A -5E57F7A3369B51636E91471C3873C4017B7694FC074E1620AD288548282201860304FA42804049A974E8D495C9DFDDC2 -62E153886C36AD29DDFBE061800863C12DB1E04E10E082A4FED50BE36ACCF2D1 -B0699C0D88A3E91683D90D0B8E2BC78829E12691A9D44B771D0FCC6A8E6E5F34 -5039B897208F5224F8147B443BF2E4AECBC4FF6BB8C5BA330094C7B1426E88EF -3F1B2AE46A6467BFE35EA4003788A2F437A7AEB45B3EB0B53D4236445165CF93 -CFB07A55170A6CC9DD578E294087FCCE356A5C11D6BDC4728BC3CE28C1AD4E0AF8CE283915464C2BC2BD47CF93B2FAE2 -EB7F7622C0A1C32B630F923CAB2A8014868212F7B3CE2DA3A81BC0BA11FE26EB -ABEBD5C5EF32E24841BDAD3A412F4CF841CB0FBC515E47F73DB55A8E3ABE8854 -099226A6C87F2E543BEF871491CBB18F4CB3AC96B0629F45D79021183EC35B34C48E03E8C8EA5E11171D6ACC233293D7 -AE78F4F9F800E1A2F96C2A1ADC2655576B723773D533DF2B6A9D98004F607990675648FEA66470FE70E32CCD071D7DEB -AC4065F0A4CD5947DE081FEC2B35F3C1A30A3C2A12908F59B7B4A1C2AA5A1C56 -15791F43991B06D729EAD37E9990D6FCF20F356D03661B4B96A5777D9FE6A7EA -78AADF788C3AE3E619078B0AF71AAB39E125A6046B4FC362D7C8D51EC8C32451 -8B9000CF70198026159D56F7B4A530BCE0E247C00CD4D81EEB59ED52C57FEA03 -A41F06335E36DDEED8E77A6773E75C8BB9E255DB48A070DDB63CCA4D8DA00336 -33DB24D54DA53E295467FAB962D863C7015DB706E4AF6248B9B046FB7B9899C2 -3C4C7C892BAA22AAEF6C92962C67CA0B0F18D8C0A88FD45A4D0A5A166FF1C766 -9B0850A575A27BA23E82E52EAECC6766B0D5FB3CDCBC60CD970AB90742B5BCFA -9329F044763196FC029639758C29F4186F77AF3DAF237CA434427A712A6BA890 -35F89F373EA9F7EBAF1DE48BC21E14D7D1B28967C8F65A5D0228CED120ABD06F -319423ACE5A80A117C7521A1A0BF22B40181B62BEBDB800AEE139E94FE323298 -3C2D4F8700825C966004993A1191F1573CBF4A407CB7728937ED0E46320D4E9B -C1A9BF45B7E5AFE2753D7C8E04C80301EF54FBD9377F1A879C224A6CD0C841C9B8DAFEF2E12B4D0418D36B3AF8AFCBB7 -062E33ADC270AF5111536CADF329D9C78E3CC3FB0538ACF38E9E2A61B5A3B49E -BC6E26CCC82DB4C4AD1970CDCED894E85F838EFF0701A36BCFCBFA463A7D0482 -CDBD96D21F2CD5457863AE80240A435E478FEE7F7130ED22253B9FA77BED9B67 -21956DAF6FE1C313DBF310EC05A6EAD9930B6571AAEAB968C26CA4348ACECFE4 -F05A6468310D49A770A4FFB960EDD2F985646C83C7A7F5160E61CE9BDC578D38 -7F7BEC5B03AFE530746A0F2E8277D17FAA743D678D0FDB69626FFA4AB07504B4 -92204CD4721F426DE2870D47FDDB54771BF8BF766AE2B88C1A2AE1A27ADAEBB6B50BB0D2A64E50725A8613DC702B4645 -A3EA461CED048BA13FEFD357C04EC1CFD8C3608D5B5C81577C36B8C7B9A80759 -960FA991AC2669E25D03AF0B52D04691172A00EECE05DC6DD59D008CEF220780 -EFE29D8222D2C319C93A0601410A1ED88563CD38E3E4E621617AFB72E3C100F3 -C55ADFA945416FFBD013F6B97914B4A902FABBFB4EFBB2583AEE11D93DC87B4D -6D462090B80489D07337E034F0F27C881066321C58C553EDCBCE593A50B96A31 -C2DB9833D0810D023E548EF4BEC218D916DB005EF2BE1122AE4BC756EBE4AFBF -D5E0EC7C772A9E39973EF35DA201C661BCD182DA2AAE3E3706781191666B7B4F -371463A5E41EEEA81D25217FDB97DACABC0B1892E0A5B36825B83330FEB900D3D89915D7B9AD5B3C3550C84384DF242E -BEE891192062A5AA254B6E3AD27D006A650D947EDF9010F6A78C78A4443F1D0C -D56C5498E65B3E2FE6B1A3C25B4D47650DD9E1BAE04AA9CD468026854D5B231B -2CFF607EE43B721D9B6F564CF70034920716392BAD686A95518CD02ACFE6D232 -2FABCA8EA65BF98CB8236AD4301B8D1C474FBB1F33B084C736C323A83B6DE336 -07AF12E594D474483BB6DB6CD2F7F722934DF2B9BECE2F0761FED2ECA2738C17FA8B03195A632D5395E9D2F4F8B84C91 +IV: 43 68 65 63 6B 20 70 61 73 73 65 64 20 4F 77 4F +00A92D8AE07E4998E826AF5C89AD659BD8C2BC6A40DA78B1AC05CF5B066243ED +A3C71FA36145D0EE56D78C05DDDDA97E487BCDCA6BDAC2C6F87402A0B20EE3CB +B9BDFBD18A63503580C18ABF84101B33D7F720900201510086EB3F0C1F533564 +EAB736F80371447008CC3BE3CE952CE429E7BF743A70C7CC62FF415B9E38467B +9C50D75C6E4A82F49AF285EC3545E58F8815FF4FCF5C9FFFCF0151FB693413FF13B594C8A28077450C8E561B0272C264 +A54C7E6DD1CAE0758F7A123B187C6EDA5BF3789969FE1E5F5A167E2DA7719671 +1461891D4AB3500B5BC0859166377CE098C04AAF6FD721F9C58A155F23F8E75A +BD1FD5645367BA76D8A87C271FF71E71C407B276BB0B165AF8CF6317250B77A6 +B18718EA6EB53CCDF9C26FC46E36D17234D93EF578376123B1F3F9953302CF62 +B633458C1948BC65357904F901F6DD5CD9D795887C176C6AA48E477F0EB693DB +B1352AC0DE8EBDD838F5DF7E040B062CF8FBB73180F3E712C5B2BDBFE1257A2C18694C51F242BCEA62DF317708771AA1 +842294FA0BACCD5B7710F002E40E4BB6142BC7C3125B2E56D4431FD8D6DD1CCE3397A4EB502B9E4248FA68CE8013E93D +5379E56AD483C0F870D9446C9CDE65ACA40C3A699D0653EC2F356A076D72D6C2 +8FD394F533CD2B03AE247B18F4A5214ED0AF64892F497362129FEF5837012BE4 +FA8BAB9CD11B306F9FCD924B3A66C678A0316F048A30312B5FA69B9E86EDD8C9 +35D6E447F3B8BA11331805027BCA1D972E9B29E07F1D2993C974DCAF5EE1EB76 +5FBF06E449C471D0AE2DECD141B937666C30EE72273CF58A99E19DCF5D2F69A703F17880B5FB1873480AFA82E2CE7674 +06C1463057544BDB86BD91B3BCC507FD3A5939CFF3315AFD252511FE4F6109EC +9AB5C03053AF5836E71FE27D0CD74515C967273EB240B7C37825B94D9D9F08AC +66EB1C65F32D5DBDC37B5FF7ACD7C4273B88298E8F0BED08F533F41F38DE651010686B79623804F67398B020E109B9A4 +7B1846759EDD928EF35B2E8FB7E3FFFE545C9A8B349476A9CE2BDDE55CE0E97F53002A543D0FB738CA67490501629296 +EB61CB652F414A70441ADACA46DBCC78A5AAC7A2ED4527AA2A93995482985867 +1E119E453C38C3242EFE3D9A3BBB3D257D91D15710C47811C1ADA934515DBD9E +6D8B689C37F7CFD52123BA77B6A4FABB16C3D22BF66FB78B4387BDBC975A3EE5 +997A46848917B76A1D728DA8C3A072F16F020AF50070BAD91AC2D841FA9805C1 +C51BFD0B93FECC7234D342F1DA785736A9229A21ACA8C9AA171906AA8916856F +86188B0DF2C25CA48DC0ED0B4524C17D93585873877FAFBBB55EB24A1D6FDABC +8178B7DFF235094BDCA1E5D72E48B113F2290C0FEBC95CF842013AB9CD84E0AD +25AEF7C490788DCC142CBC96FBCAB598B7EA1D15B7DE4ABBF75DE70832B519E4 +8C5FB85C16C5C833E1D8CE13A9432009D98B0D1639EB0C7C86D0AF4ACD7EB694 +AC9422E946BB8482CFD808B8E17BE01D9F5D9AB49E1192174A04BB0F032F2182 +793EEE939544D18C547E2198FA6F4A72D518C15C14146FC6CAD6AB642A3C9824 +C910617B5DC2C3137E96AC1869B7C5E90A1585181CC1B585C4CACC2624B7A72A +0AAEBC3463300A0391C2B2B14865A68EF44EAE8B1C2462B2730E28B25881B6462B3CCA631DC8A750F97EF5003E6B3060 +0861D4A3FE5970BB36BF8B525C69009212520A3B79F48ACEC6CB8F6DF96D913F +6327F56523F609D4ABC552912EF808E5919EA42104137AD206EF93AA4B34C097 +F88B71DAE547754731CD1C45ACBC355E52FE6D7B984A27B454DD7E8BFF12A023 +824025D56ED8B11BB65722F8C04168767D059BEB156B183D718CFB6AA38B9D81 +66E60820D0A6C452D9E209FB56FE3F49CAEFCBDC6116177063E0759FA11FDC8F +52D6ACA1A6928C52462BABD9A76628B25A2B913FC5BE316A9248C90B6F952529 +6FF5E9DAC956EF849FB58C11948C6F00743E157A8C2C631769EFCD80BAA3C048E10E682437EB53ED906C3060CCC7FECC +42757F22BDC1059154C41EDEE74E227E6D980F39939D3AA78FEC3F4DC303BE87 +689C2694447C4FF56C85B60E9FA66CD70A7EB9703A7C0202AD34374E9B290178 +E1A8E0ACF1E30714A7E580CA6380879C48992BA93A1637F24A66E15A03509B96 +5E1176A42E6C0DCF80E7B445372E3C054875A39D5F7A8B9C35985EDB46F65BB0 +12A6349CB3EC6D79B3C85052D6155730A036AF7F54BCD8CC9EC8F36557005D5E +B6D3D039D8B6D6F298665B16EBCB908D5F64D37C31F583C4F660AC0535DEC2DD +327BEE9B3E7A989B49787DE9D0C573C97343D79527FA6F442F8BDA0B9336E666 +1BDEDA4766FB3EFC1867836267E632B12020262B1EABA4BD31F9AB20DE49632A +8DE80C00CAD7F6E555713414DE8CE56B1C8603777F38F553F528907FA25E153EA5D3459739C66B46F6854A75F19A1511 +3A07DD2876F794FDF920F5D7B50A6D15C618FCF02F064E5AD5871C5B098EE8EC +66B7166156874006B35AF6997F61B84F016868FBAD283304B256F3DA065E65E3 +1E6D841D11C065F88B3F52EF60862E579E717E6AEFABF6A79FCF90D0810B35BB +B3B168E2532A6B659503736AA8933FB01A88DB9EA339C1C19EF7566600312B26 +974628362FD1951C207BDC3F5363F2747BAFA14ED7155772ADAA2FE8D1F7DC702F8DDB8CB4C17DBA803EC0DB6CDA43AC diff --git a/applications/services/dolphin/dolphin.c b/applications/services/dolphin/dolphin.c index 5d8dc61cb9f..09feee40f9c 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 6db4d824120..e85ff2b209d 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 3d84b549955..a5f87d75c38 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 8b791622646..3dc1546546d 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 873bb37a868..3960d93b7c6 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 189bf24daba..b73c4a1dda0 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -191,7 +191,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; @@ -223,8 +223,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 cc4b9703dcb..1d6db35a7b2 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 00000000000..206848aa3e4 --- /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 00000000000..455f1deea36 --- /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 00000000000..db404ec8eb9 --- /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 00000000000..7b096ef7066 --- /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 00000000000..13a1c3395e9 --- /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 00000000000..d0582252c49 --- /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 00000000000..496b3f7af8d --- /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 00000000000..81cf58a748e --- /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 00000000000..9ab5773a5ef --- /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 00000000000..1ae1dee0e62 --- /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 c5f81defa6f..9a7ae40b18d 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 36fd7b16c4a..73bdde21eed 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( @@ -30,7 +41,7 @@ App( appid="js_gui", apptype=FlipperAppType.PLUGIN, entry_point="js_gui_ep", - requires=["js_app", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"], ) @@ -38,7 +49,7 @@ App( appid="js_gui__loading", apptype=FlipperAppType.PLUGIN, entry_point="js_view_loading_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/loading.c"], ) @@ -46,7 +57,7 @@ App( appid="js_gui__empty_screen", apptype=FlipperAppType.PLUGIN, entry_point="js_view_empty_screen_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/empty_screen.c"], ) @@ -54,7 +65,7 @@ App( appid="js_gui__submenu", apptype=FlipperAppType.PLUGIN, entry_point="js_view_submenu_ep", - requires=["js_app", "js_gui"], + requires=["js_app"], sources=["modules/js_gui/submenu.c"], ) @@ -62,10 +73,18 @@ App( appid="js_gui__text_input", apptype=FlipperAppType.PLUGIN, entry_point="js_view_text_input_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/text_input.c"], ) +App( + appid="js_gui__byte_input", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_byte_input_ep", + requires=["js_app"], + sources=["modules/js_gui/byte_input.c"], +) + App( appid="js_gui__text_box", apptype=FlipperAppType.PLUGIN, @@ -82,6 +101,15 @@ App( sources=["modules/js_gui/dialog.c"], ) +App( + appid="js_gui__file_picker", + apptype=FlipperAppType.PLUGIN, + entry_point="js_gui_file_picker_ep", + requires=["js_app"], + sources=["modules/js_gui/file_picker.c"], + fap_libs=["assets"], +) + App( appid="js_notification", apptype=FlipperAppType.PLUGIN, @@ -110,7 +138,7 @@ App( appid="js_gpio", apptype=FlipperAppType.PLUGIN, entry_point="js_gpio_ep", - requires=["js_app", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gpio.c"], ) diff --git a/applications/system/js_app/examples/apps/Scripts/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/badusb_demo.js index 7284d86b741..d1ace384586 100644 --- a/applications/system/js_app/examples/apps/Scripts/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/badusb_demo.js @@ -13,7 +13,13 @@ let views = { }), }; -badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" }); +badusb.setup({ + vid: 0xAAAA, + pid: 0xBBBB, + mfrName: "Flipper", + prodName: "Zero", + layoutPath: "/ext/badusb/assets/layouts/en-US.kl" +}); eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) { if (button !== "center") @@ -39,7 +45,13 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) badusb.println("Flipper Model: " + flipper.getModel()); badusb.println("Flipper Name: " + flipper.getName()); - badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%"); + badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%"); + + // Alt+Numpad method works only on Windows!!! + badusb.altPrintln("This was printed with Alt+Numpad method!"); + + // There's also badusb.print() and badusb.altPrint() + // which don't add the return at the end notify.success(); } else { @@ -47,6 +59,9 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) notify.error(); } + // Optional, but allows to unlock usb interface to switch profile + badusb.quit(); + eventLoop.stop(); }, eventLoop, gui); diff --git a/applications/system/js_app/examples/apps/Scripts/gpio.js b/applications/system/js_app/examples/apps/Scripts/gpio.js index f3b4bc121bb..24d0f028682 100644 --- a/applications/system/js_app/examples/apps/Scripts/gpio.js +++ b/applications/system/js_app/examples/apps/Scripts/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/gui.js b/applications/system/js_app/examples/apps/Scripts/gui.js index dd80b5bc4b5..a1e023853ec 100644 --- a/applications/system/js_app/examples/apps/Scripts/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/gui.js @@ -5,8 +5,11 @@ let loadingView = require("gui/loading"); let submenuView = require("gui/submenu"); let emptyView = require("gui/empty_screen"); let textInputView = require("gui/text_input"); +let byteInputView = require("gui/byte_input"); let textBoxView = require("gui/text_box"); let dialogView = require("gui/dialog"); +let filePicker = require("gui/file_picker"); +let flipper = require("flipper"); // declare view instances let views = { @@ -16,9 +19,14 @@ let views = { header: "Enter your name", minLength: 0, maxLength: 32, + defaultText: flipper.getName(), + defaultTextClear: true, }), - helloDialog: dialogView.makeWith({ - center: "Hi Flipper! :)", + helloDialog: dialogView.make(), + bytekb: byteInputView.makeWith({ + header: "Look ma, I'm a header text!", + length: 8, + defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), }), longText: textBoxView.makeWith({ text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.", @@ -29,7 +37,9 @@ let views = { "Hourglass screen", "Empty screen", "Text input & Dialog", + "Byte input", "Text box", + "File picker", "Exit app", ], }), @@ -49,15 +59,28 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v } else if (index === 2) { gui.viewDispatcher.switchTo(views.keyboard); } else if (index === 3) { - gui.viewDispatcher.switchTo(views.longText); + gui.viewDispatcher.switchTo(views.bytekb); } else if (index === 4) { + gui.viewDispatcher.switchTo(views.longText); + } else if (index === 5) { + let path = filePicker.pickFile("/ext", "*"); + if (path) { + views.helloDialog.set("text", "You selected:\n" + path); + } else { + views.helloDialog.set("text", "You didn't select a file"); + } + views.helloDialog.set("center", "Nice!"); + gui.viewDispatcher.switchTo(views.helloDialog); + } else if (index === 6) { eventLoop.stop(); } }, gui, eventLoop, views); // say hi after keyboard input eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) { + views.keyboard.set("defaultText", name); // Remember for next usage views.helloDialog.set("text", "Hi " + name + "! :)"); + views.helloDialog.set("center", "Hi Flipper! :)"); gui.viewDispatcher.switchTo(views.helloDialog); }, gui, views); @@ -67,10 +90,26 @@ eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) gui.viewDispatcher.switchTo(views.demos); }, gui, views); +// show data after byte input +eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) { + let data_view = Uint8Array(data); + let text = "0x"; + for (let i = 0; i < data_view.length; i++) { + text += data_view[i].toString(16); + } + views.helloDialog.set("text", "You typed:\n" + text); + views.helloDialog.set("center", "Cool!"); + gui.viewDispatcher.switchTo(views.helloDialog); +}, gui, views); + // go to the demo chooser screen when the back key is pressed -eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) { +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views, eventLoop) { + if (gui.viewDispatcher.currentView === views.demos) { + eventLoop.stop(); + return; + } gui.viewDispatcher.switchTo(views.demos); -}, gui, views); +}, gui, views, eventLoop); // run UI gui.viewDispatcher.switchTo(views.demos); 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 00000000000..40ca98c3091 --- /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/load.js b/applications/system/js_app/examples/apps/Scripts/load.js index 813619741af..82b2d20461d 100644 --- a/applications/system/js_app/examples/apps/Scripts/load.js +++ b/applications/system/js_app/examples/apps/Scripts/load.js @@ -1,3 +1,3 @@ -let math = load("/ext/apps/Scripts/load_api.js"); +let 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 00000000000..0be31b81d14 --- /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 00000000000..c0ec8bfa45a --- /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 00000000000..b2facb237b0 --- /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/examples/apps/Scripts/uart_echo.js b/applications/system/js_app/examples/apps/Scripts/uart_echo.js index 06d6119fdcb..d43c0baa345 100644 --- a/applications/system/js_app/examples/apps/Scripts/uart_echo.js +++ b/applications/system/js_app/examples/apps/Scripts/uart_echo.js @@ -6,6 +6,9 @@ while (1) { if (rx_data !== undefined) { serial.write(rx_data); let data_view = Uint8Array(rx_data); - print("0x" + toString(data_view[0], 16)); + print("0x" + data_view[0].toString(16)); } } + +// There's also serial.end(), so you can serial.setup() again in same script +// You can also use serial.readAny(timeout), will avoid starving your loop with single byte reads diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index 5de720b4379..c321150df7b 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -97,7 +97,7 @@ static void js_app_free(JsApp* app) { int32_t js_app(void* arg) { JsApp* app = js_app_alloc(); - FuriString* script_path = furi_string_alloc_set(APP_ASSETS_PATH()); + FuriString* script_path = furi_string_alloc_set(EXT_PATH("apps/Scripts")); do { if(arg != NULL && strlen(arg) > 0) { furi_string_set(script_path, (const char*)arg); diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 38ff46f752b..bffa553a8c1 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 788715872e6..1dfd59521d8 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 7e7280e9cb1..97c9f742502 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include #include @@ -194,6 +196,27 @@ static void js_require(struct mjs* mjs) { mjs_return(mjs, req_object); } +static void js_parse_int(struct mjs* mjs) { + const char* str; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str)); + + int32_t base = 10; + if(mjs_nargs(mjs) >= 2) { + mjs_val_t base_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(base_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number"); + mjs_return(mjs, MJS_UNDEFINED); + } + base = mjs_get_int(mjs, base_arg); + } + + int32_t num; + if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { + num = 0; + } + mjs_return(mjs, mjs_mk_number(mjs, num)); +} + static void js_global_to_string(struct mjs* mjs) { int base = 10; if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); @@ -231,18 +254,49 @@ static int32_t js_thread(void* arg) { struct mjs* mjs = mjs_create(worker); worker->modules = js_modules_create(mjs, worker->resolver); mjs_val_t global = mjs_get_global(mjs); - mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); - mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); - mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string)); - mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); - mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); - mjs_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 891bfa2cdfc..27f38cbda73 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -2,8 +2,11 @@ #include "../js_modules.h" #include +#define ASCII_TO_KEY(layout, x) (((uint8_t)x < 128) ? (layout[(uint8_t)x]) : HID_KEYBOARD_NONE) + typedef struct { FuriHalUsbHidConfig* hid_cfg; + uint16_t layout[128]; FuriHalUsbInterface* usb_if_prev; uint8_t key_hold_cnt; } JsBadusbInst; @@ -64,9 +67,36 @@ static const struct { {"F22", HID_KEYBOARD_F22}, {"F23", HID_KEYBOARD_F23}, {"F24", HID_KEYBOARD_F24}, + + {"NUM0", HID_KEYPAD_0}, + {"NUM1", HID_KEYPAD_1}, + {"NUM2", HID_KEYPAD_2}, + {"NUM3", HID_KEYPAD_3}, + {"NUM4", HID_KEYPAD_4}, + {"NUM5", HID_KEYPAD_5}, + {"NUM6", HID_KEYPAD_6}, + {"NUM7", HID_KEYPAD_7}, + {"NUM8", HID_KEYPAD_8}, + {"NUM9", HID_KEYPAD_9}, }; -static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConfig* hid_cfg) { +static void js_badusb_quit_free(JsBadusbInst* badusb) { + if(badusb->usb_if_prev) { + furi_hal_hid_kb_release_all(); + furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL)); + badusb->usb_if_prev = NULL; + } + if(badusb->hid_cfg) { + free(badusb->hid_cfg); + badusb->hid_cfg = NULL; + } +} + +static bool setup_parse_params( + JsBadusbInst* badusb, + struct mjs* mjs, + mjs_val_t arg, + FuriHalUsbHidConfig* hid_cfg) { if(!mjs_is_object(arg)) { return false; } @@ -74,6 +104,7 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0); mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0); mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0); + mjs_val_t layout_obj = mjs_get(mjs, arg, "layoutPath", ~0); if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) { hid_cfg->vid = mjs_get_int32(mjs, vid_obj); @@ -100,6 +131,25 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product)); } + if(mjs_is_string(layout_obj)) { + size_t str_len = 0; + const char* str_temp = mjs_get_string(mjs, &layout_obj, &str_len); + if((str_len == 0) || (str_temp == NULL)) { + return false; + } + File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); + bool layout_loaded = storage_file_open(file, str_temp, FSAM_READ, FSOM_OPEN_EXISTING) && + storage_file_read(file, badusb->layout, sizeof(badusb->layout)) == + sizeof(badusb->layout); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + if(!layout_loaded) { + return false; + } + } else { + memcpy(badusb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(badusb->layout))); + } + return true; } @@ -122,7 +172,7 @@ static void js_badusb_setup(struct mjs* mjs) { } else if(num_args == 1) { badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig)); // Parse argument object - args_correct = setup_parse_params(mjs, mjs_arg(mjs, 0), badusb->hid_cfg); + args_correct = setup_parse_params(badusb, mjs, mjs_arg(mjs, 0), badusb->hid_cfg); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -142,6 +192,22 @@ static void js_badusb_setup(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); @@ -157,9 +223,9 @@ static void js_badusb_is_connected(struct mjs* mjs) { mjs_return(mjs, mjs_mk_boolean(mjs, is_connected)); } -uint16_t get_keycode_by_name(const char* key_name, size_t name_len) { +uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) { if(name_len == 1) { // Single char - return HID_ASCII_TO_KEY(key_name[0]); + return (ASCII_TO_KEY(badusb->layout, key_name[0])); } for(size_t i = 0; i < COUNT_OF(key_codes); i++) { @@ -176,7 +242,7 @@ uint16_t get_keycode_by_name(const char* key_name, size_t name_len) { return HID_KEYBOARD_NONE; } -static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) { +static bool parse_keycode(JsBadusbInst* badusb, struct mjs* mjs, size_t nargs, uint16_t* keycode) { uint16_t key_tmp = 0; for(size_t i = 0; i < nargs; i++) { mjs_val_t arg = mjs_arg(mjs, i); @@ -187,7 +253,7 @@ static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) { // String error return false; } - uint16_t str_key = get_keycode_by_name(key_name, name_len); + uint16_t str_key = get_keycode_by_name(badusb, key_name, name_len); if(str_key == HID_KEYBOARD_NONE) { // Unknown key code return false; @@ -225,7 +291,7 @@ static void js_badusb_press(struct mjs* mjs) { uint16_t keycode = HID_KEYBOARD_NONE; size_t num_args = mjs_nargs(mjs); if(num_args > 0) { - args_correct = parse_keycode(mjs, num_args, &keycode); + args_correct = parse_keycode(badusb, mjs, num_args, &keycode); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -251,7 +317,7 @@ static void js_badusb_hold(struct mjs* mjs) { uint16_t keycode = HID_KEYBOARD_NONE; size_t num_args = mjs_nargs(mjs); if(num_args > 0) { - args_correct = parse_keycode(mjs, num_args, &keycode); + args_correct = parse_keycode(badusb, mjs, num_args, &keycode); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -290,7 +356,7 @@ static void js_badusb_release(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); return; } else { - args_correct = parse_keycode(mjs, num_args, &keycode); + args_correct = parse_keycode(badusb, mjs, num_args, &keycode); } if(!args_correct) { mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); @@ -304,7 +370,35 @@ static void js_badusb_release(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } -static void badusb_print(struct mjs* mjs, bool ln) { +// Make sure NUMLOCK is enabled for altchar +static void ducky_numlock_on() { + if((furi_hal_hid_get_led_state() & HID_KB_LED_NUM) == 0) { + furi_hal_hid_kb_press(HID_KEYBOARD_LOCK_NUM_LOCK); + furi_hal_hid_kb_release(HID_KEYBOARD_LOCK_NUM_LOCK); + } +} + +// Simulate pressing a character using ALT+Numpad ASCII code +static void ducky_altchar(JsBadusbInst* badusb, const char* ascii_code) { + // Hold the ALT key + furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT); + + // Press the corresponding numpad key for each digit of the ASCII code + for(size_t i = 0; ascii_code[i] != '\0'; i++) { + char digitChar[5] = {'N', 'U', 'M', ascii_code[i], '\0'}; // Construct the numpad key name + uint16_t numpad_keycode = get_keycode_by_name(badusb, digitChar, strlen(digitChar)); + if(numpad_keycode == HID_KEYBOARD_NONE) { + continue; // Skip if keycode not found + } + furi_hal_hid_kb_press(numpad_keycode); + furi_hal_hid_kb_release(numpad_keycode); + } + + // Release the ALT key + furi_hal_hid_kb_release(KEY_MOD_LEFT_ALT); +} + +static void badusb_print(struct mjs* mjs, bool ln, bool alt) { 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); @@ -350,10 +444,20 @@ static void badusb_print(struct mjs* mjs, bool ln) { return; } + if(alt) { + ducky_numlock_on(); + } for(size_t i = 0; i < text_len; i++) { - uint16_t keycode = HID_ASCII_TO_KEY(text_str[i]); - furi_hal_hid_kb_press(keycode); - furi_hal_hid_kb_release(keycode); + if(alt) { + // Convert character to ascii numeric value + char ascii_str[4]; + snprintf(ascii_str, sizeof(ascii_str), "%u", (uint8_t)text_str[i]); + ducky_altchar(badusb, ascii_str); + } else { + uint16_t keycode = ASCII_TO_KEY(badusb->layout, text_str[i]); + furi_hal_hid_kb_press(keycode); + furi_hal_hid_kb_release(keycode); + } if(delay_val > 0) { bool need_exit = js_delay_with_flags(mjs, delay_val); if(need_exit) { @@ -371,11 +475,19 @@ static void badusb_print(struct mjs* mjs, bool ln) { } static void js_badusb_print(struct mjs* mjs) { - badusb_print(mjs, false); + badusb_print(mjs, false, false); } static void js_badusb_println(struct mjs* mjs) { - badusb_print(mjs, true); + badusb_print(mjs, true, false); +} + +static void js_badusb_alt_print(struct mjs* mjs) { + badusb_print(mjs, false, true); +} + +static void js_badusb_alt_println(struct mjs* mjs) { + badusb_print(mjs, true, true); } static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { @@ -384,25 +496,22 @@ static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod mjs_val_t badusb_obj = mjs_mk_object(mjs); mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb)); mjs_set(mjs, badusb_obj, "setup", ~0, MJS_MK_FN(js_badusb_setup)); + mjs_set(mjs, badusb_obj, "quit", ~0, MJS_MK_FN(js_badusb_quit)); mjs_set(mjs, badusb_obj, "isConnected", ~0, MJS_MK_FN(js_badusb_is_connected)); mjs_set(mjs, badusb_obj, "press", ~0, MJS_MK_FN(js_badusb_press)); mjs_set(mjs, badusb_obj, "hold", ~0, MJS_MK_FN(js_badusb_hold)); mjs_set(mjs, badusb_obj, "release", ~0, MJS_MK_FN(js_badusb_release)); mjs_set(mjs, badusb_obj, "print", ~0, MJS_MK_FN(js_badusb_print)); mjs_set(mjs, badusb_obj, "println", ~0, MJS_MK_FN(js_badusb_println)); + mjs_set(mjs, badusb_obj, "altPrint", ~0, MJS_MK_FN(js_badusb_alt_print)); + mjs_set(mjs, badusb_obj, "altPrintln", ~0, MJS_MK_FN(js_badusb_alt_println)); *object = badusb_obj; return badusb; } static void js_badusb_destroy(void* inst) { JsBadusbInst* badusb = inst; - if(badusb->usb_if_prev) { - furi_hal_hid_kb_release_all(); - furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL)); - } - if(badusb->hid_cfg) { - free(badusb->hid_cfg); - } + js_badusb_quit_free(badusb); free(badusb); } 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 c4f0d1beec9..7f45c1a0f3f 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 43c675e107c..eeaa2c8a02d 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 70021968fa8..ae3fefd71e7 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) { @@ -269,12 +269,11 @@ static void js_gpio_get(struct mjs* mjs) { manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); manager_data->adc_handle = module->adc_handle; manager_data->adc_channel = pin_record->channel; - mjs_own(mjs, &manager); mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); - mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog)); + mjs_set(mjs, manager, "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/byte_input.c b/applications/system/js_app/modules/js_gui/byte_input.c new file mode 100644 index 00000000000..2d6dae47559 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/byte_input.c @@ -0,0 +1,158 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#define DEFAULT_BUF_SZ 4 + +typedef struct { + uint8_t* buffer; + size_t buffer_size; + size_t default_data_size; + FuriString* header; + FuriSemaphore* input_semaphore; + JsEventLoopContract contract; +} JsByteKbContext; + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsByteKbContext* context) { + furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk); + return mjs_mk_array_buf(mjs, (char*)context->buffer, context->buffer_size); +} + +static void input_callback(JsByteKbContext* context) { + furi_semaphore_release(context->input_semaphore); +} + +static bool header_assign( + struct mjs* mjs, + ByteInput* input, + JsViewPropValue value, + JsByteKbContext* context) { + UNUSED(mjs); + furi_string_set(context->header, value.string); + byte_input_set_header_text(input, furi_string_get_cstr(context->header)); + return true; +} + +static bool + len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) { + UNUSED(mjs); + UNUSED(input); + size_t new_buffer_size = value.number; + if(new_buffer_size < context->default_data_size) { + // Avoid confusing parameters from user + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "length must be larger than defaultData length"); + return false; + } + context->buffer_size = new_buffer_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + byte_input_set_result_callback( + input, + (ByteInputCallback)input_callback, + NULL, + context, + context->buffer, + context->buffer_size); + return true; +} + +static bool default_data_assign( + struct mjs* mjs, + ByteInput* input, + JsViewPropValue value, + JsByteKbContext* context) { + UNUSED(mjs); + + mjs_val_t array_buf = value.term; + if(mjs_is_data_view(array_buf)) { + array_buf = mjs_dataview_get_buf(mjs, array_buf); + } + char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &context->default_data_size); + if(context->buffer_size < context->default_data_size) { + // Ensure buffer is large enough for defaultData + context->buffer_size = context->default_data_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + } + memcpy(context->buffer, (uint8_t*)default_data, context->default_data_size); + if(context->buffer_size > context->default_data_size) { + // Reset previous data after defaultData + memset( + context->buffer + context->default_data_size, + 0x00, + context->buffer_size - context->default_data_size); + } + + byte_input_set_result_callback( + input, + (ByteInputCallback)input_callback, + NULL, + context, + context->buffer, + context->buffer_size); + return true; +} + +static JsByteKbContext* ctx_make(struct mjs* mjs, ByteInput* input, mjs_val_t view_obj) { + JsByteKbContext* context = malloc(sizeof(JsByteKbContext)); + *context = (JsByteKbContext){ + .buffer_size = DEFAULT_BUF_SZ, + .buffer = malloc(DEFAULT_BUF_SZ), + .header = furi_string_alloc(), + .input_semaphore = furi_semaphore_alloc(1, 0), + }; + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeSemaphore, + .object = context->input_semaphore, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + .transformer_context = context, + }, + }; + byte_input_set_result_callback( + input, + (ByteInputCallback)input_callback, + NULL, + context, + context->buffer, + context->buffer_size); + mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(ByteInput* input, JsByteKbContext* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore); + furi_semaphore_free(context->input_semaphore); + furi_string_free(context->header); + free(context->buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)byte_input_alloc, + .free = (JsViewFree)byte_input_free, + .get_view = (JsViewGetView)byte_input_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 3, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)header_assign}, + (JsViewPropDescriptor){ + .name = "length", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)len_assign}, + (JsViewPropDescriptor){ + .name = "defaultData", + .type = JsViewPropTypeTypedArr, + .assign = (JsViewPropAssign)default_data_assign}, + }}; + +JS_GUI_VIEW_DEF(byte_input, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/file_picker.c b/applications/system/js_app/modules/js_gui/file_picker.c new file mode 100644 index 00000000000..49cf5e89dca --- /dev/null +++ b/applications/system/js_app/modules/js_gui/file_picker.c @@ -0,0 +1,47 @@ +#include "../../js_modules.h" +#include +#include + +static void js_gui_file_picker_pick_file(struct mjs* mjs) { + const char *base_path, *extension; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&base_path), JS_ARG_STR(&extension)); + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + const DialogsFileBrowserOptions browser_options = { + .extension = extension, + .icon = &I_file_10px, + .base_path = base_path, + }; + FuriString* path = furi_string_alloc_set(base_path); + if(dialog_file_browser_show(dialogs, path, path, &browser_options)) { + mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true)); + } else { + mjs_return(mjs, MJS_UNDEFINED); + } + furi_string_free(path); + furi_record_close(RECORD_DIALOGS); +} + +static void* js_gui_file_picker_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + *object = mjs_mk_object(mjs); + mjs_set(mjs, *object, "pickFile", ~0, MJS_MK_FN(js_gui_file_picker_pick_file)); + return NULL; +} + +static const JsModuleDescriptor js_gui_file_picker_desc = { + "gui__file_picker", + js_gui_file_picker_create, + NULL, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_gui_file_picker_desc, +}; + +const FlipperAppPluginDescriptor* js_gui_file_picker_ep(void) { + return &plugin_descriptor; +} diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index 8ac3055d5dc..22d04855d2a 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -101,8 +101,10 @@ static void js_gui_vd_switch_to(struct mjs* mjs) { mjs_val_t view; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view)); JsGuiViewData* view_data = JS_GET_INST(mjs, view); - JsGui* module = JS_GET_CONTEXT(mjs); + mjs_val_t vd_obj = mjs_get_this(mjs); + JsGui* module = JS_GET_INST(mjs, vd_obj); view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id); + mjs_set(mjs, vd_obj, "currentView", ~0, view); } static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { @@ -154,6 +156,7 @@ static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* module JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to)); JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract)); JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract)); + JS_FIELD("currentView", MJS_NULL); } // create API object @@ -213,7 +216,21 @@ static bool expected_type = "array"; break; } - c_value = (JsViewPropValue){.array = value}; + c_value = (JsViewPropValue){.term = value}; + } break; + case JsViewPropTypeTypedArr: { + if(!mjs_is_typed_array(value)) { + expected_type = "typed_array"; + break; + } + c_value = (JsViewPropValue){.term = value}; + } break; + case JsViewPropTypeBool: { + if(!mjs_is_boolean(value)) { + expected_type = "bool"; + break; + } + c_value = (JsViewPropValue){.boolean = mjs_get_bool(mjs, value)}; } break; } diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h index 02198ca4f35..d400d0a33cf 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -9,12 +9,15 @@ typedef enum { JsViewPropTypeString, JsViewPropTypeNumber, JsViewPropTypeArr, + JsViewPropTypeTypedArr, + JsViewPropTypeBool, } JsViewPropType; typedef union { const char* string; int32_t number; - mjs_val_t array; + bool boolean; + mjs_val_t term; } JsViewPropValue; /** diff --git a/applications/system/js_app/modules/js_gui/submenu.c b/applications/system/js_app/modules/js_gui/submenu.c index aecd413be4d..c142bcddb66 100644 --- a/applications/system/js_app/modules/js_gui/submenu.c +++ b/applications/system/js_app/modules/js_gui/submenu.c @@ -33,9 +33,9 @@ static bool static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { UNUSED(mjs); submenu_reset(submenu); - size_t len = mjs_array_length(mjs, value.array); + size_t len = mjs_array_length(mjs, value.term); for(size_t i = 0; i < len; i++) { - mjs_val_t item = mjs_array_get(mjs, value.array, i); + mjs_val_t item = mjs_array_get(mjs, value.term, i); if(!mjs_is_string(item)) return false; submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context); } 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 575029f8e69..bb3a6cfa31c 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -8,7 +8,9 @@ typedef struct { char* buffer; size_t buffer_size; + size_t default_text_size; FuriString* header; + bool default_text_clear; FuriSemaphore* input_semaphore; JsEventLoopContract contract; } JsKbdContext; @@ -48,7 +50,14 @@ static bool max_len_assign( JsViewPropValue value, JsKbdContext* context) { UNUSED(mjs); - context->buffer_size = (size_t)(value.number + 1); + size_t new_buffer_size = value.number + 1; + if(new_buffer_size < context->default_text_size) { + // Avoid confusing parameters from user + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "maxLength must be larger than defaultText length"); + return false; + } + context->buffer_size = new_buffer_size; context->buffer = realloc(context->buffer, context->buffer_size); //-V701 text_input_set_result_callback( input, @@ -56,17 +65,63 @@ static bool max_len_assign( context, context->buffer, context->buffer_size, - true); + context->default_text_clear); return true; } -static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) { +static bool default_text_assign( + struct mjs* mjs, + TextInput* input, + JsViewPropValue value, + JsKbdContext* context) { + UNUSED(mjs); UNUSED(input); + + if(value.string) { + context->default_text_size = strlen(value.string) + 1; + if(context->buffer_size < context->default_text_size) { + // Ensure buffer is large enough for defaultData + context->buffer_size = context->default_text_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + } + // Also trim excess previous data with strlcpy() + strlcpy(context->buffer, value.string, context->buffer_size); //-V575 + text_input_set_result_callback( + input, + (TextInputCallback)input_callback, + context, + context->buffer, + context->buffer_size, + context->default_text_clear); + } + return true; +} + +static bool default_text_clear_assign( + struct mjs* mjs, + TextInput* input, + JsViewPropValue value, + JsKbdContext* context) { + UNUSED(mjs); + + context->default_text_clear = value.boolean; + text_input_set_result_callback( + input, + (TextInputCallback)input_callback, + context, + context->buffer, + context->buffer_size, + context->default_text_clear); + return true; +} + +static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) { JsKbdContext* context = malloc(sizeof(JsKbdContext)); *context = (JsKbdContext){ .buffer_size = DEFAULT_BUF_SZ, .buffer = malloc(DEFAULT_BUF_SZ), .header = furi_string_alloc(), + .default_text_clear = false, .input_semaphore = furi_semaphore_alloc(1, 0), }; context->contract = (JsEventLoopContract){ @@ -80,8 +135,13 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_ .transformer_context = context, }, }; - UNUSED(mjs); - UNUSED(view_obj); + text_input_set_result_callback( + input, + (TextInputCallback)input_callback, + context, + context->buffer, + context->buffer_size, + context->default_text_clear); mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); return context; } @@ -101,7 +161,7 @@ static const JsViewDescriptor view_descriptor = { .get_view = (JsViewGetView)text_input_get_view, .custom_make = (JsViewCustomMake)ctx_make, .custom_destroy = (JsViewCustomDestroy)ctx_destroy, - .prop_cnt = 3, + .prop_cnt = 5, .props = { (JsViewPropDescriptor){ .name = "header", @@ -115,6 +175,14 @@ static const JsViewDescriptor view_descriptor = { .name = "maxLength", .type = JsViewPropTypeNumber, .assign = (JsViewPropAssign)max_len_assign}, + (JsViewPropDescriptor){ + .name = "defaultText", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)default_text_assign}, + (JsViewPropDescriptor){ + .name = "defaultTextClear", + .type = JsViewPropTypeBool, + .assign = (JsViewPropAssign)default_text_clear_assign}, }}; JS_GUI_VIEW_DEF(text_input, &view_descriptor); diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index 7d54cf9b9fa..cf66b6a44d4 100644 --- a/applications/system/js_app/modules/js_math.c +++ b/applications/system/js_app/modules/js_math.c @@ -308,7 +308,7 @@ void js_math_trunc(struct mjs* mjs) { static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); mjs_val_t math_obj = mjs_mk_object(mjs); - mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal)); + mjs_set(mjs, math_obj, "isEqual", ~0, MJS_MK_FN(js_math_is_equal)); mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs)); mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos)); mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh)); diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index 7aea927837c..b1e578fbc77 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -1,4 +1,5 @@ #include +#include #include #include "../js_modules.h" #include @@ -89,14 +90,49 @@ static void js_serial_setup(struct mjs* mjs) { return; } - serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1); + expansion_disable(furi_record_open(RECORD_EXPANSION)); + furi_record_close(RECORD_EXPANSION); + serial->serial_handle = furi_hal_serial_control_acquire(serial_id); if(serial->serial_handle) { + serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1); furi_hal_serial_init(serial->serial_handle, baudrate); furi_hal_serial_async_rx_start( serial->serial_handle, js_serial_on_async_rx, serial, false); serial->setup_done = true; + } else { + expansion_enable(furi_record_open(RECORD_EXPANSION)); + furi_record_close(RECORD_EXPANSION); + } +} + +static void js_serial_deinit(JsSerialInst* js_serial) { + if(js_serial->setup_done) { + furi_hal_serial_async_rx_stop(js_serial->serial_handle); + furi_hal_serial_deinit(js_serial->serial_handle); + furi_hal_serial_control_release(js_serial->serial_handle); + js_serial->serial_handle = NULL; + furi_stream_buffer_free(js_serial->rx_stream); + + expansion_enable(furi_record_open(RECORD_EXPANSION)); + furi_record_close(RECORD_EXPANSION); + + js_serial->setup_done = false; + } +} + +static void js_serial_end(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + furi_assert(serial); + + if(!serial->setup_done) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); + mjs_return(mjs, MJS_UNDEFINED); + return; } + + js_serial_deinit(serial); } static void js_serial_write(struct mjs* mjs) { @@ -346,6 +382,55 @@ static void js_serial_read_bytes(struct mjs* mjs) { free(read_buf); } +static char* js_serial_receive_any(JsSerialInst* serial, size_t* len, uint32_t timeout) { + uint32_t flags = ThreadEventCustomDataRx; + if(furi_stream_buffer_is_empty(serial->rx_stream)) { + flags = js_flags_wait(serial->mjs, ThreadEventCustomDataRx, timeout); + } + if(flags & ThreadEventCustomDataRx) { // New data received + *len = furi_stream_buffer_bytes_available(serial->rx_stream); + if(!*len) return NULL; + char* buf = malloc(*len); + furi_stream_buffer_receive(serial->rx_stream, buf, *len, 0); + return buf; + } + return NULL; +} + +static void js_serial_read_any(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + furi_assert(serial); + if(!serial->setup_done) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint32_t timeout = FuriWaitForever; + + do { + size_t num_args = mjs_nargs(mjs); + if(num_args == 1) { + mjs_val_t timeout_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(timeout_arg)) { + break; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + } while(0); + + size_t bytes_read = 0; + char* read_buf = js_serial_receive_any(serial, &bytes_read, timeout); + + mjs_val_t return_obj = MJS_UNDEFINED; + if(bytes_read > 0 && read_buf) { + return_obj = mjs_mk_string(mjs, read_buf, bytes_read, true); + } + mjs_return(mjs, return_obj); + free(read_buf); +} + static bool js_serial_expect_parse_string(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) { size_t str_len = 0; @@ -580,10 +665,12 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod mjs_val_t serial_obj = mjs_mk_object(mjs); mjs_set(mjs, serial_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_serial)); mjs_set(mjs, serial_obj, "setup", ~0, MJS_MK_FN(js_serial_setup)); + mjs_set(mjs, serial_obj, "end", ~0, MJS_MK_FN(js_serial_end)); mjs_set(mjs, serial_obj, "write", ~0, MJS_MK_FN(js_serial_write)); mjs_set(mjs, serial_obj, "read", ~0, MJS_MK_FN(js_serial_read)); mjs_set(mjs, serial_obj, "readln", ~0, MJS_MK_FN(js_serial_readln)); mjs_set(mjs, serial_obj, "readBytes", ~0, MJS_MK_FN(js_serial_read_bytes)); + mjs_set(mjs, serial_obj, "readAny", ~0, MJS_MK_FN(js_serial_read_any)); mjs_set(mjs, serial_obj, "expect", ~0, MJS_MK_FN(js_serial_expect)); *object = serial_obj; @@ -592,14 +679,7 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod static void js_serial_destroy(void* inst) { JsSerialInst* js_serial = inst; - if(js_serial->setup_done) { - furi_hal_serial_async_rx_stop(js_serial->serial_handle); - furi_hal_serial_deinit(js_serial->serial_handle); - furi_hal_serial_control_release(js_serial->serial_handle); - js_serial->serial_handle = NULL; - } - - furi_stream_buffer_free(js_serial->rx_stream); + js_serial_deinit(js_serial); free(js_serial); } 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 00000000000..cf6ddbc91c1 --- /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 00000000000..0bfe9376e3c --- /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 00000000000..21642339656 --- /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 00000000000..58f20a385d2 --- /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 00000000000..aa57f8d03d7 --- /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 00000000000..e545841c51a --- /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 00000000000..6291e3e138d --- /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 00000000000..7acdeccaa11 --- /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 00000000000..c7b83cd5d39 --- /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 00000000000..77f12ae2e58 --- /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 00000000000..3234f68aae7 --- /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 71% 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 647382dc0b3..aa979346c92 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" | @@ -14,6 +16,9 @@ export type MainKey = "F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" | "F20" | "F21" | "F22" | "F23" | "F24" | + "NUM0" | "NUM1" | "NUM2" | "NUM3" | "NUM4" | "NUM5" | "NUM6" | "NUM7" | + "NUM8" | "NUM9" | + "\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" | "]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" | @@ -28,16 +33,19 @@ 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; /** * @brief Initializes the module * @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 }): void; +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; @@ -46,6 +54,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; @@ -54,6 +63,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; @@ -62,6 +72,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; @@ -69,6 +80,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; @@ -77,5 +89,29 @@ 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; + +/** + * @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; + +/** + * @brief Prints a string by Alt+Numpad method - works only on Windows! + * 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 00000000000..f82f58ec77f --- /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 00000000000..001518f878c --- /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 00000000000..2dac4204b10 --- /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 00000000000..953afc30d0b --- /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 18705f8982c..b484ebbf6c4 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 00000000000..5556e7fbb15 --- /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 00000000000..9bd0c3966a6 --- /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 00000000000..49e59142699 --- /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 00000000000..9447f89a916 --- /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 00000000000..3184a57180b --- /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 00000000000..b8b10c43a1d --- /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 00000000000..31e08aab8a6 --- /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 00000000000..a46ec73fab2 --- /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 00000000000..5d64b038bfe --- /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/packages/fz-sdk/math/index.d.ts b/applications/system/js_app/packages/fz-sdk/math/index.d.ts new file mode 100644 index 00000000000..67c805db570 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/math/index.d.ts @@ -0,0 +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 947daba2186..2199a14794c 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 00000000000..4d18f3f2019 --- /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 00000000000..45944a8546b --- /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 00000000000..2eecf032dcd --- /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 77% 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 1a7ed6397e3..3c249352e1f 100644 --- a/applications/system/js_app/types/serial/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts @@ -1,7 +1,17 @@ +/** + * Module for accessing the serial port + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Initializes the serial port + * + * Automatically disables Expansion module service to prevent interference. + * * @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; @@ -13,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; @@ -24,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; @@ -39,9 +51,24 @@ 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; +/** + * @brief Read any available amount of data from the serial port + * + * Can be useful to avoid starving your loop with small reads. + * + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return nothing. If 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 readAny(timeout?: number): string | undefined; + /** * @brief Reads data from the serial port * @param length The number of bytes to read @@ -50,6 +77,7 @@ export declare function readln(timeout?: number): string; * 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; @@ -73,5 +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 0dd29e121cb..90d7a82546b 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 8aaeec5e523..031588d4ae8 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 00000000000..cfb792e3f96 --- /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 00000000000..8b3befa6d13 --- /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 49237782c51..00000000000 --- 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 b1b1d474bc5..00000000000 --- 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/global.d.ts b/applications/system/js_app/types/global.d.ts deleted file mode 100644 index ab1660cf66d..00000000000 --- a/applications/system/js_app/types/global.d.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @brief Pauses JavaScript execution for a while - * @param ms How many milliseconds to pause the execution for - */ -declare function delay(ms: number): void; - -/** - * @brief Prints to the GUI console view - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the console view - */ -declare function print(...args: any[]): void; - -/** - * @brief Converts a number to a string - * @param value The number to convert to a string - * @param base Integer base (`2`...`16`), default: 16 - */ -declare function toString(value: number, base?: number): string; - -/** - * @brief Reads a JS value from a file - * - * Reads a file at the specified path, interprets it as a JS value and returns - * said value. - * - * @param path The path to the file - */ -declare function load(path: string): any; - -/** - * @brief mJS Foreign Pointer type - * - * JavaScript code cannot do anything with values of `RawPointer` type except - * acquire them from native code and pass them right back to other parts of - * native code. These values cannot be turned into something meaningful, nor can - * be they modified. - */ -declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; -// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. - -/** - * @brief Holds raw bytes - */ -declare class ArrayBuffer { - /** - * @brief The pointer to the byte buffer - * @note Like other `RawPointer` values, this value is essentially useless - * to JS code. - */ - getPtr: RawPointer; - /** - * @brief The length of the buffer in bytes - */ - byteLength: number; - /** - * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer - * @param start The index of the byte in the source buffer to be used as the - * start for the new buffer - * @param end The index of the byte in the source buffer that follows the - * byte to be used as the last byte for the new buffer - */ - slice(start: number, end?: number): ArrayBuffer; -} - -declare function ArrayBuffer(): ArrayBuffer; - -declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; - -declare class TypedArray { - /** - * @brief The length of the buffer in bytes - */ - byteLength: number; - /** - * @brief The length of the buffer in typed elements - */ - length: number; - /** - * @brief The underlying `ArrayBuffer` - */ - buffer: ArrayBuffer; -} - -declare class Uint8Array extends TypedArray<"u8"> { } -declare class Int8Array extends TypedArray<"i8"> { } -declare class Uint16Array extends TypedArray<"u16"> { } -declare class Int16Array extends TypedArray<"i16"> { } -declare class Uint32Array extends TypedArray<"u32"> { } -declare class Int32Array extends TypedArray<"i32"> { } - -declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; -declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; -declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; -declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; -declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; -declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; - -declare const console: { - /** - * @brief Prints to the UART logs at the `[I]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - log(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[D]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - debug(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[W]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - warn(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[E]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - error(...args: any[]): void; -}; - -declare class Array { - /** - * @brief Takes items out of the array - * - * Removes elements from the array and returns them in a new array - * - * @param start The index to start taking elements from - * @param deleteCount How many elements to take - * @returns The elements that were taken out of the original array as a new - * array - */ - splice(start: number, deleteCount: number): T[]; - /** - * @brief Adds a value to the end of the array - * @param value The value to add - * @returns New length of the array - */ - push(value: T): number; - /** - * @brief How many elements there are in the array - */ - length: number; -} - -declare class String { - /** - * @brief How many characters there are in the string - */ - length: number; - /** - * @brief Returns the character code at an index in the string - * @param index The index to consult - */ - charCodeAt(index: number): number; - /** - * See `charCodeAt` - */ - at(index: number): number; -} - -declare class Boolean { } - -declare class Function { } - -declare class Number { } - -declare class Object { } - -declare class RegExp { } - -declare interface IArguments { } - -declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gui/dialog.d.ts b/applications/system/js_app/types/gui/dialog.d.ts deleted file mode 100644 index 6d9c8d43b2f..00000000000 --- 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 c71e93b3271..00000000000 --- 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/index.d.ts b/applications/system/js_app/types/gui/index.d.ts deleted file mode 100644 index 3f95ab78067..00000000000 --- a/applications/system/js_app/types/gui/index.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Contract } from "../event_loop"; - -type Properties = { [K: string]: any }; - -export declare class View { - set

(property: P, value: Props[P]): void; -} - -export declare class ViewFactory> { - make(): V; - makeWith(initial: Partial): V; -} - -declare class ViewDispatcher { - /** - * Event source for `sendCustom` events - */ - custom: Contract; - /** - * Event source for navigation events (back key presses) - */ - navigation: Contract; - /** - * Sends a number to the custom event handler - * @param event number to send - */ - sendCustom(event: number): void; - /** - * Switches to a view - * @param assoc View-ViewDispatcher association as returned by `add` - */ - switchTo(assoc: View): void; - /** - * Sends this ViewDispatcher to the front or back, above or below all other - * GUI viewports - * @param direction Either `"front"` or `"back"` - */ - sendTo(direction: "front" | "back"): void; -} - -export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/types/gui/loading.d.ts b/applications/system/js_app/types/gui/loading.d.ts deleted file mode 100644 index 73a9633494c..00000000000 --- 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 59d53586431..00000000000 --- 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 3dbbac5710c..00000000000 --- 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 96652b1d454..00000000000 --- a/applications/system/js_app/types/gui/text_input.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - minLength: number, - maxLength: number, -} -declare class TextInput extends View { - input: Contract; -} -declare class TextInputFactory extends ViewFactory { } -declare const factory: TextInputFactory; -export = factory; diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/types/math/index.d.ts deleted file mode 100644 index 25abca4af14..00000000000 --- a/applications/system/js_app/types/math/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -export function abs(n: number): number; -export function acos(n: number): number; -export function acosh(n: number): number; -export function asin(n: number): number; -export function asinh(n: number): number; -export function atan(n: number): number; -export function atan2(a: number, b: number): number; -export function atanh(n: number): number; -export function cbrt(n: number): number; -export function ceil(n: number): number; -export function clz32(n: number): number; -export function cos(n: number): number; -export function exp(n: number): number; -export function floor(n: number): number; -export function max(n: number, m: number): number; -export function min(n: number, m: number): number; -export function pow(n: number, m: number): number; -export function random(): number; -export function sign(n: number): number; -export function sin(n: number): number; -export function sqrt(n: number): number; -export function trunc(n: number): number; -declare const PI: number; -declare const EPSILON: number; diff --git a/assets/icons/Archive/file_10px.png b/assets/icons/Archive/file_10px.png new file mode 100644 index 00000000000..cd38770f411 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 00000000000..56411603693 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 00000000000..c5c58ea91dd 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 00000000000..b7d338f7d4e 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 00000000000..067d55ddd03 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 00000000000..56411603693 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 00000000000..0cfbf08886f --- /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 5c80e763d4f..9711c6ae175 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/doxygen/index.dox b/documentation/doxygen/index.dox index 7bd9024a116..9587afd8b4e 100644 --- a/documentation/doxygen/index.dox +++ b/documentation/doxygen/index.dox @@ -1,7 +1,8 @@ /** -@mainpage Overview +@mainpage notitle +## Welcome to Flipper Developer Documentation! -Welcome to the Flipper Developer Documentation! +--- This documentation is intended for developers interested in modifying the Flipper Zero firmware, creating Apps and JavaScript programs, or working on external hardware modules for the device. diff --git a/documentation/images/byte_input.png b/documentation/images/byte_input.png new file mode 100644 index 00000000000..9a46eb01dcf 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 9791fb4ebcd..aa444bacdab 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 12dae8fb377..ca16a9111fb 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 19b28a5003c..721f4c2fa83 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 b622aa7a1cf..c0998ea902e 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); - - 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(); - } + FuriEventLoopProcessStatus status = furi_event_loop_process_event(instance, item); + + 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 6c5ba432c73..d5e8710a69e 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 + */ +typedef void (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); + +/** Opaque event flag type */ +typedef struct FuriEventFlag FuriEventFlag; + +/** Subscribe to event flag events * - * @return true if event was processed, false if we need to delay processing + * @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 */ -typedef bool (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); + +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 15efa8f8642..7016e1e1bea 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 992ca655559..4b993390ff1 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 9034043faf7..50fb57389fb 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 bd0cec02146..b0862e50127 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 f9848e1baa9..edaba9e0058 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 850169ad6e2..d05b9bf092a 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 f35abec647c..783b2d7413d 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/digital_signal/digital_sequence.c b/lib/digital_signal/digital_sequence.c index f93ce6d951a..14cb3aab2fc 100644 --- a/lib/digital_signal/digital_sequence.c +++ b/lib/digital_signal/digital_sequence.c @@ -15,6 +15,9 @@ * Example: * ./fbt --extra-define=DIGITAL_SIGNAL_DEBUG_OUTPUT_PIN=gpio_ext_pb3 */ +#ifdef DIGITAL_SIGNAL_DEBUG_OUTPUT_PIN +#include +#endif #define TAG "DigitalSequence" diff --git a/lib/flipper_application/flipper_application.c b/lib/flipper_application/flipper_application.c index 376e9c9ead4..77bfa438728 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 afcf9ce6f07..7a1d74ff159 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_exec.c b/lib/mjs/mjs_exec.c index 265e7d5c39b..8fdb2d7e5e0 100644 --- a/lib/mjs/mjs_exec.c +++ b/lib/mjs/mjs_exec.c @@ -452,6 +452,12 @@ static int getprop_builtin_string( } else if(strcmp(name, "slice") == 0) { *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_slice); return 1; + } else if(strcmp(name, "toUpperCase") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_upper_case); + return 1; + } else if(strcmp(name, "toLowerCase") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_lower_case); + return 1; } else if(isnum) { /* * string subscript: return a new one-byte string if the index @@ -469,6 +475,22 @@ static int getprop_builtin_string( return 0; } +static int getprop_builtin_number( + struct mjs* mjs, + mjs_val_t val, + const char* name, + size_t name_len, + mjs_val_t* res) { + if(strcmp(name, "toString") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_number_to_string); + return 1; + } + + (void)val; + (void)name_len; + return 0; +} + static int getprop_builtin_array( struct mjs* mjs, mjs_val_t val, @@ -583,6 +605,8 @@ static int getprop_builtin(struct mjs* mjs, mjs_val_t val, mjs_val_t name, mjs_v } else if(s != NULL && n == 5 && strncmp(s, "apply", n) == 0) { *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_apply_); handled = 1; + } else if(mjs_is_number(val)) { + handled = getprop_builtin_number(mjs, val, s, n, res); } else if(mjs_is_array(val)) { handled = getprop_builtin_array(mjs, val, s, n, res); } else if(mjs_is_foreign(val)) { diff --git a/lib/mjs/mjs_object.c b/lib/mjs/mjs_object.c index 60bacf51458..cf14a499f96 100644 --- a/lib/mjs/mjs_object.c +++ b/lib/mjs/mjs_object.c @@ -294,6 +294,11 @@ MJS_PRIVATE void mjs_op_create_object(struct mjs* mjs) { 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 870486d06f9..101272e2948 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 503b169426a..212804a8627 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_primitive.c b/lib/mjs/mjs_primitive.c index b63a268e567..e73ae892d3f 100644 --- a/lib/mjs/mjs_primitive.c +++ b/lib/mjs/mjs_primitive.c @@ -6,6 +6,8 @@ #include "mjs_core.h" #include "mjs_internal.h" #include "mjs_primitive.h" +#include "mjs_string_public.h" +#include "mjs_util.h" mjs_val_t mjs_mk_null(void) { return MJS_NULL; @@ -158,3 +160,31 @@ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs) { mjs_return(mjs, ret); } + +MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs) { + mjs_val_t ret = MJS_UNDEFINED; + mjs_val_t base_v = MJS_UNDEFINED; + int32_t base = 10; + int32_t num; + + /* get number from `this` */ + if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_NUMBER, NULL)) { + goto clean; + } + num = mjs_get_int32(mjs, mjs->vals.this_obj); + + if(mjs_nargs(mjs) >= 1) { + /* get base from arg 0 */ + if(!mjs_check_arg(mjs, 0, "base", MJS_TYPE_NUMBER, &base_v)) { + goto clean; + } + base = mjs_get_int(mjs, base_v); + } + + char tmp_str[] = "-2147483648"; + itoa(num, tmp_str, base); + ret = mjs_mk_string(mjs, tmp_str, ~0, true); + +clean: + mjs_return(mjs, ret); +} diff --git a/lib/mjs/mjs_primitive.h b/lib/mjs/mjs_primitive.h index 6d6f9178515..f1c3c82206a 100644 --- a/lib/mjs/mjs_primitive.h +++ b/lib/mjs/mjs_primitive.h @@ -34,6 +34,11 @@ MJS_PRIVATE void* get_ptr(mjs_val_t v); */ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs); +/* + * Implementation for JS Number.toString() + */ +MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs); + #if defined(__cplusplus) } #endif /* __cplusplus */ diff --git a/lib/mjs/mjs_string.c b/lib/mjs/mjs_string.c index f74bf1074fb..f771ff4ea6d 100644 --- a/lib/mjs/mjs_string.c +++ b/lib/mjs/mjs_string.c @@ -286,6 +286,41 @@ MJS_PRIVATE mjs_val_t s_concat(struct mjs* mjs, mjs_val_t a, mjs_val_t b) { return res; } +MJS_PRIVATE void mjs_string_to_case(struct mjs* mjs, bool upper) { + mjs_val_t ret = MJS_UNDEFINED; + size_t size; + const char* s = NULL; + + /* get string from `this` */ + if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_STRING, NULL)) { + goto clean; + } + s = mjs_get_string(mjs, &mjs->vals.this_obj, &size); + + if(size == 0) { + ret = mjs_mk_string(mjs, "", 0, 1); + goto clean; + } + + char* tmp = malloc(size); + for(size_t i = 0; i < size; i++) { + tmp[i] = upper ? toupper(s[i]) : tolower(s[i]); + } + ret = mjs_mk_string(mjs, tmp, size, 1); + free(tmp); + +clean: + mjs_return(mjs, ret); +} + +MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs) { + mjs_string_to_case(mjs, false); +} + +MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs) { + mjs_string_to_case(mjs, true); +} + MJS_PRIVATE void mjs_string_slice(struct mjs* mjs) { int nargs = mjs_nargs(mjs); mjs_val_t ret = mjs_mk_number(mjs, 0); diff --git a/lib/mjs/mjs_string.h b/lib/mjs/mjs_string.h index ba6869b6247..f0801f1a478 100644 --- a/lib/mjs/mjs_string.h +++ b/lib/mjs/mjs_string.h @@ -33,6 +33,8 @@ MJS_PRIVATE void embed_string( MJS_PRIVATE void mjs_mkstr(struct mjs* mjs); +MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs); +MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs); MJS_PRIVATE void mjs_string_slice(struct mjs* mjs); MJS_PRIVATE void mjs_string_index_of(struct mjs* mjs); MJS_PRIVATE void mjs_string_char_code_at(struct mjs* mjs); diff --git a/lib/mjs/mjs_tok.c b/lib/mjs/mjs_tok.c index bdff5a86a23..f89606d234f 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 03d8fe6fa49..5ff0794928e 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/helpers/crypto1.c b/lib/nfc/helpers/crypto1.c index bd4fc8d6195..0f2b48e4e1c 100644 --- a/lib/nfc/helpers/crypto1.c +++ b/lib/nfc/helpers/crypto1.c @@ -82,7 +82,7 @@ uint32_t crypto1_word(Crypto1* crypto1, uint32_t in, int is_encrypted) { return out; } -uint32_t prng_successor(uint32_t x, uint32_t n) { +uint32_t crypto1_prng_successor(uint32_t x, uint32_t n) { SWAPENDIAN(x); while(n--) x = x >> 1 | (x >> 16 ^ x >> 18 ^ x >> 19 ^ x >> 21) << 31; @@ -169,11 +169,69 @@ void crypto1_encrypt_reader_nonce( nr[i] = byte; } - nt_num = prng_successor(nt_num, 32); + nt_num = crypto1_prng_successor(nt_num, 32); for(size_t i = 4; i < 8; i++) { - nt_num = prng_successor(nt_num, 8); + nt_num = crypto1_prng_successor(nt_num, 8); uint8_t byte = crypto1_byte(crypto, 0, 0) ^ (uint8_t)(nt_num); bool parity_bit = ((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(nt_num)) & 0x01); bit_buffer_set_byte_with_parity(out, i, byte, parity_bit); } } + +static uint8_t lfsr_rollback_bit(Crypto1* crypto1, uint32_t in, int fb) { + int out; + uint8_t ret; + uint32_t t; + + crypto1->odd &= 0xffffff; + t = crypto1->odd; + crypto1->odd = crypto1->even; + crypto1->even = t; + + out = crypto1->even & 1; + out ^= LF_POLY_EVEN & (crypto1->even >>= 1); + out ^= LF_POLY_ODD & crypto1->odd; + out ^= !!in; + out ^= (ret = crypto1_filter(crypto1->odd)) & (!!fb); + + crypto1->even |= (nfc_util_even_parity32(out)) << 23; + return ret; +} + +uint32_t crypto1_lfsr_rollback_word(Crypto1* crypto1, uint32_t in, int fb) { + uint32_t ret = 0; + for(int i = 31; i >= 0; i--) { + ret |= lfsr_rollback_bit(crypto1, BEBIT(in, i), fb) << (24 ^ i); + } + return ret; +} + +bool crypto1_nonce_matches_encrypted_parity_bits(uint32_t nt, uint32_t ks, uint8_t nt_par_enc) { + return (nfc_util_even_parity8((nt >> 24) & 0xFF) == + (((nt_par_enc >> 3) & 1) ^ FURI_BIT(ks, 16))) && + (nfc_util_even_parity8((nt >> 16) & 0xFF) == + (((nt_par_enc >> 2) & 1) ^ FURI_BIT(ks, 8))) && + (nfc_util_even_parity8((nt >> 8) & 0xFF) == + (((nt_par_enc >> 1) & 1) ^ FURI_BIT(ks, 0))); +} + +bool crypto1_is_weak_prng_nonce(uint32_t nonce) { + if(nonce == 0) return false; + uint16_t x = nonce >> 16; + x = (x & 0xff) << 8 | x >> 8; + for(uint8_t i = 0; i < 16; i++) { + x = x >> 1 | (x ^ x >> 2 ^ x >> 3 ^ x >> 5) << 15; + } + x = (x & 0xff) << 8 | x >> 8; + return x == (nonce & 0xFFFF); +} + +uint32_t crypto1_decrypt_nt_enc(uint32_t cuid, uint32_t nt_enc, MfClassicKey known_key) { + uint64_t known_key_int = bit_lib_bytes_to_num_be(known_key.data, 6); + Crypto1 crypto_temp; + crypto1_init(&crypto_temp, known_key_int); + crypto1_word(&crypto_temp, nt_enc ^ cuid, 1); + uint32_t decrypted_nt_enc = + (nt_enc ^ crypto1_lfsr_rollback_word(&crypto_temp, nt_enc ^ cuid, 1)); + return decrypted_nt_enc; +} diff --git a/lib/nfc/helpers/crypto1.h b/lib/nfc/helpers/crypto1.h index e71ab9a4089..0e358581a19 100644 --- a/lib/nfc/helpers/crypto1.h +++ b/lib/nfc/helpers/crypto1.h @@ -1,5 +1,6 @@ #pragma once +#include #include #ifdef __cplusplus @@ -38,7 +39,15 @@ void crypto1_encrypt_reader_nonce( BitBuffer* out, bool is_nested); -uint32_t prng_successor(uint32_t x, uint32_t n); +uint32_t crypto1_lfsr_rollback_word(Crypto1* crypto1, uint32_t in, int fb); + +bool crypto1_nonce_matches_encrypted_parity_bits(uint32_t nt, uint32_t ks, uint8_t nt_par_enc); + +bool crypto1_is_weak_prng_nonce(uint32_t nonce); + +uint32_t crypto1_decrypt_nt_enc(uint32_t cuid, uint32_t nt_enc, MfClassicKey known_key); + +uint32_t crypto1_prng_successor(uint32_t x, uint32_t n); #ifdef __cplusplus } diff --git a/lib/nfc/helpers/nfc_util.c b/lib/nfc/helpers/nfc_util.c index f502b4bfb4e..80af5cf1185 100644 --- a/lib/nfc/helpers/nfc_util.c +++ b/lib/nfc/helpers/nfc_util.c @@ -13,6 +13,10 @@ static const uint8_t nfc_util_odd_byte_parity[256] = { 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1}; +uint8_t nfc_util_even_parity8(uint8_t data) { + return !nfc_util_odd_byte_parity[data]; +} + uint8_t nfc_util_even_parity32(uint32_t data) { // data ^= data >> 16; // data ^= data >> 8; diff --git a/lib/nfc/helpers/nfc_util.h b/lib/nfc/helpers/nfc_util.h index f8e86d86585..4abde4521f6 100644 --- a/lib/nfc/helpers/nfc_util.h +++ b/lib/nfc/helpers/nfc_util.h @@ -6,6 +6,8 @@ extern "C" { #endif +uint8_t nfc_util_even_parity8(uint8_t data); + uint8_t nfc_util_even_parity32(uint32_t data); uint8_t nfc_util_odd_parity8(uint8_t data); diff --git a/lib/nfc/protocols/mf_classic/mf_classic.c b/lib/nfc/protocols/mf_classic/mf_classic.c index 4f92201e342..b1c5c20c9bc 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic.c +++ b/lib/nfc/protocols/mf_classic/mf_classic.c @@ -543,6 +543,22 @@ void mf_classic_set_key_not_found( } } +MfClassicKey + mf_classic_get_key(const MfClassicData* data, uint8_t sector_num, MfClassicKeyType key_type) { + furi_check(data); + furi_check(sector_num < mf_classic_get_total_sectors_num(data->type)); + furi_check(key_type == MfClassicKeyTypeA || key_type == MfClassicKeyTypeB); + + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector_num); + + if(key_type == MfClassicKeyTypeA) { + return sector_trailer->key_a; + } else { + return sector_trailer->key_b; + } +} + bool mf_classic_is_block_read(const MfClassicData* data, uint8_t block_num) { furi_check(data); diff --git a/lib/nfc/protocols/mf_classic/mf_classic.h b/lib/nfc/protocols/mf_classic/mf_classic.h index 801ec1764d8..6ae7a623e0b 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic.h +++ b/lib/nfc/protocols/mf_classic/mf_classic.h @@ -6,14 +6,16 @@ extern "C" { #endif -#define MF_CLASSIC_CMD_AUTH_KEY_A (0x60U) -#define MF_CLASSIC_CMD_AUTH_KEY_B (0x61U) -#define MF_CLASSIC_CMD_READ_BLOCK (0x30U) -#define MF_CLASSIC_CMD_WRITE_BLOCK (0xA0U) -#define MF_CLASSIC_CMD_VALUE_DEC (0xC0U) -#define MF_CLASSIC_CMD_VALUE_INC (0xC1U) -#define MF_CLASSIC_CMD_VALUE_RESTORE (0xC2U) -#define MF_CLASSIC_CMD_VALUE_TRANSFER (0xB0U) +#define MF_CLASSIC_CMD_AUTH_KEY_A (0x60U) +#define MF_CLASSIC_CMD_AUTH_KEY_B (0x61U) +#define MF_CLASSIC_CMD_BACKDOOR_AUTH_KEY_A (0x64U) +#define MF_CLASSIC_CMD_BACKDOOR_AUTH_KEY_B (0x65U) +#define MF_CLASSIC_CMD_READ_BLOCK (0x30U) +#define MF_CLASSIC_CMD_WRITE_BLOCK (0xA0U) +#define MF_CLASSIC_CMD_VALUE_DEC (0xC0U) +#define MF_CLASSIC_CMD_VALUE_INC (0xC1U) +#define MF_CLASSIC_CMD_VALUE_RESTORE (0xC2U) +#define MF_CLASSIC_CMD_VALUE_TRANSFER (0xB0U) #define MF_CLASSIC_CMD_HALT_MSB (0x50) #define MF_CLASSIC_CMD_HALT_LSB (0x00) @@ -211,6 +213,9 @@ void mf_classic_set_key_not_found( uint8_t sector_num, MfClassicKeyType key_type); +MfClassicKey + mf_classic_get_key(const MfClassicData* data, uint8_t sector_num, MfClassicKeyType key_type); + bool mf_classic_is_block_read(const MfClassicData* data, uint8_t block_num); void mf_classic_set_block_read(MfClassicData* data, uint8_t block_num, MfClassicBlock* block_data); diff --git a/lib/nfc/protocols/mf_classic/mf_classic_listener.c b/lib/nfc/protocols/mf_classic/mf_classic_listener.c index 7e4f4725b29..ef571117a1b 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_listener.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_listener.c @@ -157,14 +157,17 @@ static MfClassicListenerCommand uint32_t nt_num = bit_lib_bytes_to_num_be(instance->auth_context.nt.data, sizeof(MfClassicNt)); uint32_t secret_poller = ar_num ^ crypto1_word(instance->crypto, 0, 0); - if(secret_poller != prng_successor(nt_num, 64)) { + if(secret_poller != crypto1_prng_successor(nt_num, 64)) { FURI_LOG_T( - TAG, "Wrong reader key: %08lX != %08lX", secret_poller, prng_successor(nt_num, 64)); + TAG, + "Wrong reader key: %08lX != %08lX", + secret_poller, + crypto1_prng_successor(nt_num, 64)); command = MfClassicListenerCommandSleep; break; } - uint32_t at_num = prng_successor(nt_num, 96); + uint32_t at_num = crypto1_prng_successor(nt_num, 96); bit_lib_num_to_bytes_be(at_num, sizeof(uint32_t), instance->auth_context.at.data); bit_buffer_copy_bytes( instance->tx_plain_buffer, instance->auth_context.at.data, sizeof(MfClassicAr)); diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 8c50230ca05..ec37c80150e 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -6,8 +6,24 @@ #define TAG "MfClassicPoller" +// TODO FL-3926: Buffer writes for Hardnested, set state to Log when finished and sum property matches +// TODO FL-3926: Store target key in CUID dictionary +// TODO FL-3926: Dead code for malloc returning NULL? +// TODO FL-3926: Auth1 static encrypted exists (rare) +// TODO FL-3926: Use keys found by NFC plugins, cached keys + #define MF_CLASSIC_MAX_BUFF_SIZE (64) +// Ordered by frequency, labeled chronologically +const MfClassicBackdoorKeyPair mf_classic_backdoor_keys[] = { + {{{0xa3, 0x96, 0xef, 0xa4, 0xe2, 0x4f}}, MfClassicBackdoorAuth3}, // Fudan (static encrypted) + {{{0xa3, 0x16, 0x67, 0xa8, 0xce, 0xc1}}, MfClassicBackdoorAuth1}, // Fudan, Infineon, NXP + {{{0x51, 0x8b, 0x33, 0x54, 0xe7, 0x60}}, MfClassicBackdoorAuth2}, // Fudan +}; +const size_t mf_classic_backdoor_keys_count = COUNT_OF(mf_classic_backdoor_keys); +const uint16_t valid_sums[] = + {0, 32, 56, 64, 80, 96, 104, 112, 120, 128, 136, 144, 152, 160, 176, 192, 200, 224, 256}; + typedef NfcCommand (*MfClassicPollerReadHandler)(MfClassicPoller* instance); MfClassicPoller* mf_classic_poller_alloc(Iso14443_3aPoller* iso14443_3a_poller) { @@ -49,6 +65,26 @@ void mf_classic_poller_free(MfClassicPoller* instance) { bit_buffer_free(instance->tx_encrypted_buffer); bit_buffer_free(instance->rx_encrypted_buffer); + // Clean up resources in MfClassicPollerDictAttackContext + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + + // Free the dictionaries + if(dict_attack_ctx->mf_classic_system_dict) { + keys_dict_free(dict_attack_ctx->mf_classic_system_dict); + dict_attack_ctx->mf_classic_system_dict = NULL; + } + if(dict_attack_ctx->mf_classic_user_dict) { + keys_dict_free(dict_attack_ctx->mf_classic_user_dict); + dict_attack_ctx->mf_classic_user_dict = NULL; + } + + // Free the nested nonce array if it exists + if(dict_attack_ctx->nested_nonce.nonces) { + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + free(instance); } @@ -58,6 +94,11 @@ static NfcCommand mf_classic_poller_handle_data_update(MfClassicPoller* instance mf_classic_get_read_sectors_and_keys( instance->data, &data_update->sectors_read, &data_update->keys_found); data_update->current_sector = instance->mode_ctx.dict_attack_ctx.current_sector; + data_update->nested_phase = instance->mode_ctx.dict_attack_ctx.nested_phase; + data_update->prng_type = instance->mode_ctx.dict_attack_ctx.prng_type; + data_update->backdoor = instance->mode_ctx.dict_attack_ctx.backdoor; + data_update->nested_target_key = instance->mode_ctx.dict_attack_ctx.nested_target_key; + data_update->msb_count = instance->mode_ctx.dict_attack_ctx.msb_count; instance->mfc_event.type = MfClassicPollerEventTypeDataUpdate; return instance->callback(instance->general_event, instance->context); } @@ -86,7 +127,8 @@ NfcCommand mf_classic_poller_handler_detect_type(MfClassicPoller* instance) { iso14443_3a_copy( instance->data->iso14443_3a_data, iso14443_3a_poller_get_data(instance->iso14443_3a_poller)); - MfClassicError error = mf_classic_poller_get_nt(instance, 254, MfClassicKeyTypeA, NULL); + MfClassicError error = + mf_classic_poller_get_nt(instance, 254, MfClassicKeyTypeA, NULL, false); if(error == MfClassicErrorNone) { instance->data->type = MfClassicType4k; instance->state = MfClassicPollerStateStart; @@ -96,7 +138,8 @@ NfcCommand mf_classic_poller_handler_detect_type(MfClassicPoller* instance) { instance->current_type_check = MfClassicType1k; } } else if(instance->current_type_check == MfClassicType1k) { - MfClassicError error = mf_classic_poller_get_nt(instance, 62, MfClassicKeyTypeA, NULL); + MfClassicError error = + mf_classic_poller_get_nt(instance, 62, MfClassicKeyTypeA, NULL, false); if(error == MfClassicErrorNone) { instance->data->type = MfClassicType1k; FURI_LOG_D(TAG, "1K detected"); @@ -120,9 +163,12 @@ NfcCommand mf_classic_poller_handler_start(MfClassicPoller* instance) { instance->mfc_event.type = MfClassicPollerEventTypeRequestMode; command = instance->callback(instance->general_event, instance->context); - if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttack) { + if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackStandard) { mf_classic_copy(instance->data, instance->mfc_event_data.poller_mode.data); instance->state = MfClassicPollerStateRequestKey; + } else if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackEnhanced) { + mf_classic_copy(instance->data, instance->mfc_event_data.poller_mode.data); + instance->state = MfClassicPollerStateAnalyzeBackdoor; } else if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeRead) { instance->state = MfClassicPollerStateRequestReadSector; } else if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeWrite) { @@ -236,7 +282,7 @@ NfcCommand mf_classic_poller_handler_read_block(MfClassicPoller* instance) { do { // Authenticate to sector error = mf_classic_poller_auth( - instance, write_ctx->current_block, auth_key, write_ctx->key_type_read, NULL); + instance, write_ctx->current_block, auth_key, write_ctx->key_type_read, NULL, false); if(error != MfClassicErrorNone) { FURI_LOG_D(TAG, "Failed to auth to block %d", write_ctx->current_block); instance->state = MfClassicPollerStateFail; @@ -294,7 +340,12 @@ NfcCommand mf_classic_poller_handler_write_block(MfClassicPoller* instance) { // Reauth if necessary if(write_ctx->need_halt_before_write) { error = mf_classic_poller_auth( - instance, write_ctx->current_block, auth_key, write_ctx->key_type_write, NULL); + instance, + write_ctx->current_block, + auth_key, + write_ctx->key_type_write, + NULL, + false); if(error != MfClassicErrorNone) { FURI_LOG_D( TAG, "Failed to auth to block %d for writing", write_ctx->current_block); @@ -403,8 +454,8 @@ NfcCommand mf_classic_poller_handler_write_value_block(MfClassicPoller* instance MfClassicKey* key = (auth_key_type == MfClassicKeyTypeA) ? &write_ctx->sec_tr.key_a : &write_ctx->sec_tr.key_b; - MfClassicError error = - mf_classic_poller_auth(instance, write_ctx->current_block, key, auth_key_type, NULL); + MfClassicError error = mf_classic_poller_auth( + instance, write_ctx->current_block, key, auth_key_type, NULL, false); if(error != MfClassicErrorNone) break; error = mf_classic_poller_value_cmd(instance, write_ctx->current_block, value_cmd, diff); @@ -468,7 +519,8 @@ NfcCommand mf_classic_poller_handler_request_read_sector_blocks(MfClassicPoller* sec_read_ctx->current_block, &sec_read_ctx->key, sec_read_ctx->key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) break; sec_read_ctx->auth_passed = true; @@ -505,6 +557,128 @@ NfcCommand mf_classic_poller_handler_request_read_sector_blocks(MfClassicPoller* return command; } +NfcCommand mf_classic_poller_handler_analyze_backdoor(MfClassicPoller* instance) { + NfcCommand command = NfcCommandReset; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + instance->mode_ctx.dict_attack_ctx.enhanced_dict = true; + + size_t current_key_index = + mf_classic_backdoor_keys_count - 1; // Default to the last valid index + + // Find the current key in the backdoor_keys array + for(size_t i = 0; i < mf_classic_backdoor_keys_count; i++) { + if(memcmp( + dict_attack_ctx->current_key.data, + mf_classic_backdoor_keys[i].key.data, + sizeof(MfClassicKey)) == 0) { + current_key_index = i; + break; + } + } + + // Choose the next key to try + size_t next_key_index = (current_key_index + 1) % mf_classic_backdoor_keys_count; + uint8_t backdoor_version = mf_classic_backdoor_keys[next_key_index].type - 1; + + FURI_LOG_D(TAG, "Trying backdoor v%d", backdoor_version); + dict_attack_ctx->current_key = mf_classic_backdoor_keys[next_key_index].key; + + // Attempt backdoor authentication + MfClassicError error = mf_classic_poller_auth( + instance, 0, &dict_attack_ctx->current_key, MfClassicKeyTypeA, NULL, true); + if((next_key_index == 0) && + (error == MfClassicErrorProtocol || error == MfClassicErrorTimeout)) { + FURI_LOG_D(TAG, "No backdoor identified"); + dict_attack_ctx->backdoor = MfClassicBackdoorNone; + instance->state = MfClassicPollerStateRequestKey; + } else if(error == MfClassicErrorNone) { + FURI_LOG_I(TAG, "Backdoor identified: v%d", backdoor_version); + dict_attack_ctx->backdoor = mf_classic_backdoor_keys[next_key_index].type; + instance->state = MfClassicPollerStateBackdoorReadSector; + } else if( + (error == MfClassicErrorAuth) && + (next_key_index == (mf_classic_backdoor_keys_count - 1))) { + // We've tried all backdoor keys, this is a unique key and an important research finding + furi_crash("New backdoor: please report!"); + } + + return command; +} + +NfcCommand mf_classic_poller_handler_backdoor_read_sector(MfClassicPoller* instance) { + // TODO FL-3926: Reauth not needed + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + MfClassicError error = MfClassicErrorNone; + MfClassicBlock block = {}; + + uint8_t current_sector = mf_classic_get_sector_by_block(dict_attack_ctx->current_block); + uint8_t blocks_in_sector = mf_classic_get_blocks_num_in_sector(current_sector); + uint8_t first_block_in_sector = mf_classic_get_first_block_num_of_sector(current_sector); + + do { + if(dict_attack_ctx->current_block >= instance->sectors_total * 4) { + // We've read all blocks, reset current_block and move to next state + dict_attack_ctx->current_block = 0; + instance->state = MfClassicPollerStateNestedController; + break; + } + + // Authenticate with the backdoor key + error = mf_classic_poller_auth( + instance, + first_block_in_sector, // Authenticate to the first block of the sector + &(dict_attack_ctx->current_key), + MfClassicKeyTypeA, + NULL, + true); + + if(error != MfClassicErrorNone) { + FURI_LOG_E( + TAG, "Failed to authenticate with backdoor key for sector %d", current_sector); + break; + } + + // Read all blocks in the sector + for(uint8_t block_in_sector = 0; block_in_sector < blocks_in_sector; block_in_sector++) { + uint8_t block_to_read = first_block_in_sector + block_in_sector; + + error = mf_classic_poller_read_block(instance, block_to_read, &block); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to read block %d", block_to_read); + break; + } + + // Set the block as read in the data structure + mf_classic_set_block_read(instance->data, block_to_read, &block); + } + + if(error != MfClassicErrorNone) { + break; + } + + // Move to the next sector + current_sector++; + dict_attack_ctx->current_block = mf_classic_get_first_block_num_of_sector(current_sector); + + // Update blocks_in_sector and first_block_in_sector for the next sector + if(current_sector < instance->sectors_total) { + blocks_in_sector = mf_classic_get_blocks_num_in_sector(current_sector); + first_block_in_sector = mf_classic_get_first_block_num_of_sector(current_sector); + } + + // Halt the card after each sector to reset the authentication state + mf_classic_poller_halt(instance); + + // Send an event to the app that a sector has been read + command = mf_classic_poller_handle_data_update(instance); + + } while(false); + + return command; +} + NfcCommand mf_classic_poller_handler_request_key(MfClassicPoller* instance) { NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; @@ -535,7 +709,7 @@ NfcCommand mf_classic_poller_handler_auth_a(MfClassicPoller* instance) { FURI_LOG_D(TAG, "Auth to block %d with key A: %06llx", block, key); MfClassicError error = mf_classic_poller_auth( - instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeA, NULL); + instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeA, NULL, false); if(error == MfClassicErrorNone) { FURI_LOG_I(TAG, "Key A found"); mf_classic_set_key_found( @@ -574,7 +748,7 @@ NfcCommand mf_classic_poller_handler_auth_b(MfClassicPoller* instance) { FURI_LOG_D(TAG, "Auth to block %d with key B: %06llx", block, key); MfClassicError error = mf_classic_poller_auth( - instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeB, NULL); + instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeB, NULL, false); if(error == MfClassicErrorNone) { FURI_LOG_I(TAG, "Key B found"); mf_classic_set_key_found( @@ -629,7 +803,8 @@ NfcCommand mf_classic_poller_handler_read_sector(MfClassicPoller* instance) { block_num, &dict_attack_ctx->current_key, dict_attack_ctx->current_key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) { instance->state = MfClassicPollerStateNextSector; FURI_LOG_W(TAG, "Failed to re-auth. Go to next sector"); @@ -680,24 +855,51 @@ NfcCommand mf_classic_poller_handler_key_reuse_start(MfClassicPoller* instance) NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; - if(dict_attack_ctx->current_key_type == MfClassicKeyTypeA) { - dict_attack_ctx->current_key_type = MfClassicKeyTypeB; - instance->state = MfClassicPollerStateKeyReuseAuthKeyB; - } else { - dict_attack_ctx->reuse_key_sector++; - if(dict_attack_ctx->reuse_key_sector == instance->sectors_total) { - instance->mfc_event.type = MfClassicPollerEventTypeKeyAttackStop; - command = instance->callback(instance->general_event, instance->context); - instance->state = MfClassicPollerStateRequestKey; + do { + if(dict_attack_ctx->current_key_type == MfClassicKeyTypeA) { + dict_attack_ctx->current_key_type = MfClassicKeyTypeB; + instance->state = MfClassicPollerStateKeyReuseAuthKeyB; } else { - instance->mfc_event.type = MfClassicPollerEventTypeKeyAttackStart; - instance->mfc_event_data.key_attack_data.current_sector = - dict_attack_ctx->reuse_key_sector; - command = instance->callback(instance->general_event, instance->context); + dict_attack_ctx->reuse_key_sector++; + if(dict_attack_ctx->reuse_key_sector == instance->sectors_total) { + instance->mfc_event.type = MfClassicPollerEventTypeKeyAttackStop; + command = instance->callback(instance->general_event, instance->context); + // Nested entrypoint + bool nested_active = dict_attack_ctx->nested_phase != MfClassicNestedPhaseNone; + if((dict_attack_ctx->enhanced_dict) && + ((nested_active && + (dict_attack_ctx->nested_phase != MfClassicNestedPhaseFinished)) || + (!(nested_active) && !(mf_classic_is_card_read(instance->data))))) { + instance->state = MfClassicPollerStateNestedController; + break; + } + instance->state = MfClassicPollerStateRequestKey; + } else { + instance->mfc_event.type = MfClassicPollerEventTypeKeyAttackStart; + instance->mfc_event_data.key_attack_data.current_sector = + dict_attack_ctx->reuse_key_sector; + command = instance->callback(instance->general_event, instance->context); - dict_attack_ctx->current_key_type = MfClassicKeyTypeA; - instance->state = MfClassicPollerStateKeyReuseAuthKeyA; + dict_attack_ctx->current_key_type = MfClassicKeyTypeA; + instance->state = MfClassicPollerStateKeyReuseAuthKeyA; + } } + } while(false); + + return command; +} + +NfcCommand mf_classic_poller_handler_key_reuse_start_no_offset(MfClassicPoller* instance) { + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + + instance->mfc_event.type = MfClassicPollerEventTypeKeyAttackStart; + instance->mfc_event_data.key_attack_data.current_sector = dict_attack_ctx->reuse_key_sector; + command = instance->callback(instance->general_event, instance->context); + if(dict_attack_ctx->current_key_type == MfClassicKeyTypeA) { + instance->state = MfClassicPollerStateKeyReuseAuthKeyA; + } else { + instance->state = MfClassicPollerStateKeyReuseAuthKeyB; } return command; @@ -718,7 +920,7 @@ NfcCommand mf_classic_poller_handler_key_reuse_auth_key_a(MfClassicPoller* insta FURI_LOG_D(TAG, "Key attack auth to block %d with key A: %06llx", block, key); MfClassicError error = mf_classic_poller_auth( - instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeA, NULL); + instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeA, NULL, false); if(error == MfClassicErrorNone) { FURI_LOG_I(TAG, "Key A found"); mf_classic_set_key_found( @@ -754,7 +956,7 @@ NfcCommand mf_classic_poller_handler_key_reuse_auth_key_b(MfClassicPoller* insta FURI_LOG_D(TAG, "Key attack auth to block %d with key B: %06llx", block, key); MfClassicError error = mf_classic_poller_auth( - instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeB, NULL); + instance, block, &dict_attack_ctx->current_key, MfClassicKeyTypeB, NULL, false); if(error == MfClassicErrorNone) { FURI_LOG_I(TAG, "Key B found"); mf_classic_set_key_found( @@ -793,7 +995,8 @@ NfcCommand mf_classic_poller_handler_key_reuse_read_sector(MfClassicPoller* inst block_num, &dict_attack_ctx->current_key, dict_attack_ctx->current_key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) { instance->state = MfClassicPollerStateKeyReuseStart; break; @@ -829,6 +1032,1100 @@ NfcCommand mf_classic_poller_handler_key_reuse_read_sector(MfClassicPoller* inst return command; } +// Helper function to add a nonce to the array +static bool add_nested_nonce( + MfClassicNestedNonceArray* array, + uint32_t cuid, + uint16_t key_idx, + uint32_t nt, + uint32_t nt_enc, + uint8_t par, + uint16_t dist) { + MfClassicNestedNonce* new_nonces; + if(array->count == 0) { + new_nonces = malloc(sizeof(MfClassicNestedNonce)); + } else { + new_nonces = realloc(array->nonces, (array->count + 1) * sizeof(MfClassicNestedNonce)); + } + if(new_nonces == NULL) return false; + + array->nonces = new_nonces; + array->nonces[array->count].cuid = cuid; + array->nonces[array->count].key_idx = key_idx; + array->nonces[array->count].nt = nt; + array->nonces[array->count].nt_enc = nt_enc; + array->nonces[array->count].par = par; + array->nonces[array->count].dist = dist; + array->count++; + return true; +} + +NfcCommand mf_classic_poller_handler_nested_analyze_prng(MfClassicPoller* instance) { + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + uint8_t hard_nt_count = 0; + + for(uint8_t i = 0; i < dict_attack_ctx->nested_nonce.count; i++) { + MfClassicNestedNonce* nonce = &dict_attack_ctx->nested_nonce.nonces[i]; + if(!crypto1_is_weak_prng_nonce(nonce->nt)) hard_nt_count++; + } + + if(hard_nt_count >= MF_CLASSIC_NESTED_NT_HARD_MINIMUM) { + dict_attack_ctx->prng_type = MfClassicPrngTypeHard; + FURI_LOG_D(TAG, "Detected Hard PRNG"); + } else { + dict_attack_ctx->prng_type = MfClassicPrngTypeWeak; + FURI_LOG_D(TAG, "Detected Weak PRNG"); + } + + instance->state = MfClassicPollerStateNestedController; + return command; +} + +NfcCommand mf_classic_poller_handler_nested_collect_nt(MfClassicPoller* instance) { + NfcCommand command = NfcCommandReset; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + + MfClassicNt nt = {}; + MfClassicError error = mf_classic_poller_get_nt(instance, 0, MfClassicKeyTypeA, &nt, false); + + if(error != MfClassicErrorNone) { + dict_attack_ctx->prng_type = MfClassicPrngTypeNoTag; + FURI_LOG_E(TAG, "Failed to collect nt"); + } else { + FURI_LOG_T(TAG, "nt: %02x%02x%02x%02x", nt.data[0], nt.data[1], nt.data[2], nt.data[3]); + uint32_t nt_data = bit_lib_bytes_to_num_be(nt.data, sizeof(MfClassicNt)); + if(!add_nested_nonce( + &dict_attack_ctx->nested_nonce, + iso14443_3a_get_cuid(instance->data->iso14443_3a_data), + 0, + nt_data, + 0, + 0, + 0)) { + dict_attack_ctx->prng_type = MfClassicPrngTypeNoTag; + } + } + + instance->state = MfClassicPollerStateNestedController; + return command; +} + +NfcCommand mf_classic_poller_handler_nested_calibrate(MfClassicPoller* instance) { + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + uint32_t nt_enc_temp_arr[MF_CLASSIC_NESTED_CALIBRATION_COUNT]; + uint16_t distances[MF_CLASSIC_NESTED_CALIBRATION_COUNT - 1] = {0}; + + dict_attack_ctx->d_min = UINT16_MAX; + dict_attack_ctx->d_max = 0; + uint8_t block = + mf_classic_get_first_block_num_of_sector(dict_attack_ctx->nested_known_key_sector); + uint32_t cuid = iso14443_3a_get_cuid(instance->data->iso14443_3a_data); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error; + + uint32_t nt_prev = 0; + uint32_t nt_enc_prev = 0; + uint32_t same_nt_enc_cnt = 0; + uint8_t nt_enc_collected = 0; + bool use_backdoor = (dict_attack_ctx->backdoor != MfClassicBackdoorNone); + + // Step 1: Perform full authentication once + error = mf_classic_poller_auth( + instance, + block, + &dict_attack_ctx->nested_known_key, + dict_attack_ctx->nested_known_key_type, + &auth_ctx, + use_backdoor); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform full authentication"); + instance->state = MfClassicPollerStateNestedCalibrate; + return command; + } + + FURI_LOG_D(TAG, "Full authentication successful"); + + nt_prev = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + + if((dict_attack_ctx->static_encrypted) && + (dict_attack_ctx->backdoor == MfClassicBackdoorAuth3)) { + command = NfcCommandReset; + uint8_t target_block = + mf_classic_get_first_block_num_of_sector(dict_attack_ctx->nested_target_key / 4); + MfClassicKeyType target_key_type = + ((dict_attack_ctx->nested_target_key % 4) < 2) ? MfClassicKeyTypeA : MfClassicKeyTypeB; + error = mf_classic_poller_auth_nested( + instance, + target_block, + &dict_attack_ctx->nested_known_key, + target_key_type, + &auth_ctx, + use_backdoor, + false); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform nested authentication for static encrypted tag"); + instance->state = MfClassicPollerStateNestedCalibrate; + return command; + } + + uint32_t nt_enc = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + // Store the decrypted static encrypted nonce + dict_attack_ctx->static_encrypted_nonce = + crypto1_decrypt_nt_enc(cuid, nt_enc, dict_attack_ctx->nested_known_key); + + dict_attack_ctx->calibrated = true; + + FURI_LOG_D(TAG, "Static encrypted tag calibrated. Decrypted nonce: %08lx", nt_enc); + + instance->state = MfClassicPollerStateNestedController; + return command; + } + + // Original calibration logic for non-static encrypted tags + // Step 2: Perform nested authentication multiple times + for(uint8_t collection_cycle = 0; collection_cycle < MF_CLASSIC_NESTED_CALIBRATION_COUNT; + collection_cycle++) { + error = mf_classic_poller_auth_nested( + instance, + block, + &dict_attack_ctx->nested_known_key, + dict_attack_ctx->nested_known_key_type, + &auth_ctx, + use_backdoor, + false); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform nested authentication %u", collection_cycle); + continue; + } + + nt_enc_temp_arr[collection_cycle] = + bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + nt_enc_collected++; + } + + for(int i = 0; i < nt_enc_collected; i++) { + if(nt_enc_temp_arr[i] == nt_enc_prev) { + same_nt_enc_cnt++; + if(same_nt_enc_cnt > 3) { + dict_attack_ctx->static_encrypted = true; + break; + } + } else { + same_nt_enc_cnt = 0; + nt_enc_prev = nt_enc_temp_arr[i]; + } + } + + if(dict_attack_ctx->static_encrypted) { + FURI_LOG_D(TAG, "Static encrypted nonce detected"); + dict_attack_ctx->calibrated = true; + instance->state = MfClassicPollerStateNestedController; + return command; + } + + // Find the distance between each nonce + FURI_LOG_D(TAG, "Calculating distance between nonces"); + uint64_t known_key = bit_lib_bytes_to_num_be(dict_attack_ctx->nested_known_key.data, 6); + uint8_t valid_distances = 0; + for(uint32_t collection_cycle = 1; collection_cycle < MF_CLASSIC_NESTED_CALIBRATION_COUNT; + collection_cycle++) { + bool found = false; + uint32_t decrypted_nt_enc = crypto1_decrypt_nt_enc( + cuid, nt_enc_temp_arr[collection_cycle], dict_attack_ctx->nested_known_key); + for(int i = 0; i < 65535; i++) { + uint32_t nth_successor = crypto1_prng_successor(nt_prev, i); + if(nth_successor == decrypted_nt_enc) { + FURI_LOG_D(TAG, "nt_enc (plain) %08lx", nth_successor); + FURI_LOG_D(TAG, "dist from nt prev: %i", i); + distances[valid_distances++] = i; + nt_prev = nth_successor; + found = true; + break; + } + } + if(!found) { + FURI_LOG_E( + TAG, + "Failed to find distance for nt_enc %08lx", + nt_enc_temp_arr[collection_cycle]); + FURI_LOG_E( + TAG, "using key %06llx and uid %08lx, nt_prev is %08lx", known_key, cuid, nt_prev); + } + } + + // Calculate median and standard deviation + if(valid_distances > 0) { + // Sort the distances array (bubble sort) + for(uint8_t i = 0; i < valid_distances - 1; i++) { + for(uint8_t j = 0; j < valid_distances - i - 1; j++) { + if(distances[j] > distances[j + 1]) { + uint16_t temp = distances[j]; + distances[j] = distances[j + 1]; + distances[j + 1] = temp; + } + } + } + + // Calculate median + uint16_t median = + (valid_distances % 2 == 0) ? + (distances[valid_distances / 2 - 1] + distances[valid_distances / 2]) / 2 : + distances[valid_distances / 2]; + + // Calculate standard deviation + float sum = 0, sum_sq = 0; + for(uint8_t i = 0; i < valid_distances; i++) { + sum += distances[i]; + sum_sq += (float)distances[i] * distances[i]; + } + float mean = sum / valid_distances; + float variance = (sum_sq / valid_distances) - (mean * mean); + float std_dev = sqrtf(variance); + + // Filter out values over 3 standard deviations away from the median + for(uint8_t i = 0; i < valid_distances; i++) { + if(fabsf((float)distances[i] - median) <= 3 * std_dev) { + if(distances[i] < dict_attack_ctx->d_min) dict_attack_ctx->d_min = distances[i]; + if(distances[i] > dict_attack_ctx->d_max) dict_attack_ctx->d_max = distances[i]; + } + } + + // Some breathing room + dict_attack_ctx->d_min = (dict_attack_ctx->d_min > 3) ? dict_attack_ctx->d_min - 3 : 0; + dict_attack_ctx->d_max += 3; + } + + furi_assert(dict_attack_ctx->d_min <= dict_attack_ctx->d_max); + dict_attack_ctx->calibrated = true; + instance->state = MfClassicPollerStateNestedController; + + mf_classic_poller_halt(instance); + uint16_t d_dist = dict_attack_ctx->d_max - dict_attack_ctx->d_min; + FURI_LOG_D( + TAG, + "Calibration completed: min=%u max=%u static=%s", + dict_attack_ctx->d_min, + dict_attack_ctx->d_max, + ((d_dist >= 3) && (d_dist <= 6)) ? "true" : "false"); + + return command; +} + +static inline void set_byte_found(uint8_t* found, uint8_t byte) { + SET_PACKED_BIT(found, byte); +} + +static inline bool is_byte_found(uint8_t* found, uint8_t byte) { + return GET_PACKED_BIT(found, byte) != 0; +} + +NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* instance) { + // TODO FL-3926: Handle when nonce is not collected (retry counter? Do not increment nested_target_key) + // TODO FL-3926: Look into using MfClassicNt more + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + + do { + uint8_t block = + mf_classic_get_first_block_num_of_sector(dict_attack_ctx->nested_known_key_sector); + uint32_t cuid = iso14443_3a_get_cuid(instance->data->iso14443_3a_data); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error; + + bool use_backdoor = (dict_attack_ctx->backdoor != MfClassicBackdoorNone); + bool is_weak = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; + uint8_t nonce_pair_index = is_weak ? (dict_attack_ctx->nested_target_key % 2) : 0; + uint8_t nt_enc_per_collection = + (is_weak && !(dict_attack_ctx->static_encrypted)) ? + ((dict_attack_ctx->attempt_count + 2) + nonce_pair_index) : + 1; + uint8_t target_sector = dict_attack_ctx->nested_target_key / (is_weak ? 4 : 2); + MfClassicKeyType target_key_type = + (dict_attack_ctx->nested_target_key % (is_weak ? 4 : 2) < (is_weak ? 2 : 1)) ? + MfClassicKeyTypeA : + MfClassicKeyTypeB; + uint8_t target_block = mf_classic_get_sector_trailer_num_by_sector(target_sector); + uint32_t nt_enc_temp_arr[nt_enc_per_collection]; + uint8_t nt_enc_collected = 0; + uint8_t parity = 0; + + // Step 1: Perform full authentication once + error = mf_classic_poller_auth( + instance, + block, + &dict_attack_ctx->nested_known_key, + dict_attack_ctx->nested_known_key_type, + &auth_ctx, + use_backdoor); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform full authentication"); + break; + } + + FURI_LOG_D(TAG, "Full authentication successful"); + + // Step 2: Perform nested authentication a variable number of times to get nt_enc at a different PRNG offset + // eg. Collect most commonly observed nonce from 3 auths to known sector and 4th to target, then separately the + // most commonly observed nonce from 4 auths to known sector and 5th to target. This gets us a nonce pair, + // at a known distance (confirmed by parity bits) telling us the nt_enc plain. + for(uint8_t collection_cycle = 0; collection_cycle < (nt_enc_per_collection - 1); + collection_cycle++) { + // This loop must match the calibrated loop + error = mf_classic_poller_auth_nested( + instance, + block, + &dict_attack_ctx->nested_known_key, + dict_attack_ctx->nested_known_key_type, + &auth_ctx, + use_backdoor, + false); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform nested authentication %u", collection_cycle); + break; + } + + nt_enc_temp_arr[collection_cycle] = + bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + nt_enc_collected++; + } + error = mf_classic_poller_auth_nested( + instance, + target_block, + &dict_attack_ctx->nested_known_key, + target_key_type, + &auth_ctx, + false, + true); + + if(nt_enc_collected != (nt_enc_per_collection - 1)) { + FURI_LOG_E(TAG, "Failed to collect sufficient nt_enc values"); + break; + } + + uint32_t nt_enc = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + // Collect parity bits + const uint8_t* parity_data = bit_buffer_get_parity(instance->rx_plain_buffer); + for(int i = 0; i < 4; i++) { + parity = (parity << 1) | (((parity_data[0] >> i) & 0x01) ^ 0x01); + } + + uint32_t nt_prev = 0, decrypted_nt_prev = 0, found_nt = 0; + uint16_t dist = 0; + if(is_weak && !(dict_attack_ctx->static_encrypted)) { + // Ensure this isn't the same nonce as the previous collection + if((dict_attack_ctx->nested_nonce.count == 1) && + (dict_attack_ctx->nested_nonce.nonces[0].nt_enc == nt_enc)) { + FURI_LOG_D(TAG, "Duplicate nonce, dismissing collection attempt"); + break; + } + + // Decrypt the previous nonce + nt_prev = nt_enc_temp_arr[nt_enc_collected - 1]; + decrypted_nt_prev = + crypto1_decrypt_nt_enc(cuid, nt_prev, dict_attack_ctx->nested_known_key); + + // Find matching nt_enc plain at expected distance + found_nt = 0; + uint8_t found_nt_cnt = 0; + uint16_t current_dist = dict_attack_ctx->d_min; + while(current_dist <= dict_attack_ctx->d_max) { + uint32_t nth_successor = crypto1_prng_successor(decrypted_nt_prev, current_dist); + if(crypto1_nonce_matches_encrypted_parity_bits( + nth_successor, nth_successor ^ nt_enc, parity)) { + found_nt_cnt++; + if(found_nt_cnt > 1) { + FURI_LOG_D(TAG, "Ambiguous nonce, dismissing collection attempt"); + break; + } + found_nt = nth_successor; + } + current_dist++; + } + if(found_nt_cnt != 1) { + break; + } + } else if(dict_attack_ctx->static_encrypted) { + if(dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) { + found_nt = dict_attack_ctx->static_encrypted_nonce; + } else { + dist = UINT16_MAX; + } + } else { + // Hardnested + if(!is_byte_found(dict_attack_ctx->nt_enc_msb, (nt_enc >> 24) & 0xFF)) { + set_byte_found(dict_attack_ctx->nt_enc_msb, (nt_enc >> 24) & 0xFF); + dict_attack_ctx->msb_count++; + // Add unique parity to sum + dict_attack_ctx->msb_par_sum += nfc_util_even_parity32(parity & 0x08); + } + parity ^= 0x0F; + } + + // Add the nonce to the array + if(add_nested_nonce( + &dict_attack_ctx->nested_nonce, + cuid, + dict_attack_ctx->nested_target_key, + found_nt, + nt_enc, + parity, + dist)) { + dict_attack_ctx->auth_passed = true; + } else { + FURI_LOG_E(TAG, "Failed to add nested nonce to array. OOM?"); + } + + FURI_LOG_D( + TAG, + "Target: %u (nonce pair %u, key type %s, block %u)", + dict_attack_ctx->nested_target_key, + nonce_pair_index, + (target_key_type == MfClassicKeyTypeA) ? "A" : "B", + target_block); + FURI_LOG_T(TAG, "cuid: %08lx", cuid); + FURI_LOG_T(TAG, "nt_enc: %08lx", nt_enc); + FURI_LOG_T( + TAG, + "parity: %u%u%u%u", + ((parity >> 3) & 1), + ((parity >> 2) & 1), + ((parity >> 1) & 1), + (parity & 1)); + FURI_LOG_T(TAG, "nt_enc prev: %08lx", nt_prev); + FURI_LOG_T(TAG, "nt_enc prev decrypted: %08lx", decrypted_nt_prev); + } while(false); + + instance->state = MfClassicPollerStateNestedController; + + mf_classic_poller_halt(instance); + return command; +} + +static MfClassicKey* search_dicts_for_nonce_key( + MfClassicPollerDictAttackContext* dict_attack_ctx, + MfClassicNestedNonceArray* nonce_array, + KeysDict* system_dict, + KeysDict* user_dict, + bool is_weak) { + MfClassicKey stack_key; + KeysDict* dicts[] = {user_dict, system_dict}; + bool is_resumed = dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackResume; + bool found_resume_point = false; + + for(int i = 0; i < 2; i++) { + if(!dicts[i]) continue; + keys_dict_rewind(dicts[i]); + while(keys_dict_get_next_key(dicts[i], stack_key.data, sizeof(MfClassicKey))) { + if(is_resumed && !found_resume_point) { + found_resume_point = + (memcmp( + dict_attack_ctx->current_key.data, + stack_key.data, + sizeof(MfClassicKey)) == 0); + continue; + } + bool full_match = true; + for(uint8_t j = 0; j < nonce_array->count; j++) { + // Verify nonce matches encrypted parity bits for all nonces + uint32_t nt_enc_plain = crypto1_decrypt_nt_enc( + nonce_array->nonces[j].cuid, nonce_array->nonces[j].nt_enc, stack_key); + if(is_weak) { + full_match &= crypto1_is_weak_prng_nonce(nt_enc_plain); + if(!full_match) break; + } + full_match &= crypto1_nonce_matches_encrypted_parity_bits( + nt_enc_plain, + nt_enc_plain ^ nonce_array->nonces[j].nt_enc, + nonce_array->nonces[j].par); + if(!full_match) break; + } + if(full_match) { + MfClassicKey* new_candidate = malloc(sizeof(MfClassicKey)); + if(new_candidate == NULL) return NULL; // malloc failed + memcpy(new_candidate, &stack_key, sizeof(MfClassicKey)); + return new_candidate; + } + } + } + + return NULL; +} + +NfcCommand mf_classic_poller_handler_nested_dict_attack(MfClassicPoller* instance) { + // TODO FL-3926: Handle when nonce is not collected (retry counter? Do not increment nested_target_key) + // TODO FL-3926: Look into using MfClassicNt more + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + + do { + uint8_t block = + mf_classic_get_first_block_num_of_sector(dict_attack_ctx->nested_known_key_sector); + uint32_t cuid = iso14443_3a_get_cuid(instance->data->iso14443_3a_data); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error; + + bool use_backdoor_for_initial_auth = (dict_attack_ctx->backdoor != MfClassicBackdoorNone); + bool is_weak = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; + bool is_last_iter_for_hard_key = + ((!is_weak) && ((dict_attack_ctx->nested_target_key % 8) == 7)); + MfClassicKeyType target_key_type = + (((is_weak) && ((dict_attack_ctx->nested_target_key % 2) == 0)) || + ((!is_weak) && ((dict_attack_ctx->nested_target_key % 16) < 8))) ? + MfClassicKeyTypeA : + MfClassicKeyTypeB; + uint8_t target_sector = dict_attack_ctx->nested_target_key / (is_weak ? 2 : 16); + uint8_t target_block = mf_classic_get_sector_trailer_num_by_sector(target_sector); + uint8_t parity = 0; + + if(((is_weak) && (dict_attack_ctx->nested_nonce.count == 0)) || + ((!is_weak) && (dict_attack_ctx->nested_nonce.count < 8))) { + // Step 1: Perform full authentication once + error = mf_classic_poller_auth( + instance, + block, + &dict_attack_ctx->nested_known_key, + dict_attack_ctx->nested_known_key_type, + &auth_ctx, + use_backdoor_for_initial_auth); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform full authentication"); + dict_attack_ctx->auth_passed = false; + break; + } + + FURI_LOG_D(TAG, "Full authentication successful"); + + // Step 2: Collect nested nt and parity + error = mf_classic_poller_auth_nested( + instance, + target_block, + &dict_attack_ctx->nested_known_key, + target_key_type, + &auth_ctx, + false, + true); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform nested authentication"); + dict_attack_ctx->auth_passed = false; + break; + } + + uint32_t nt_enc = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + // Collect parity bits + const uint8_t* parity_data = bit_buffer_get_parity(instance->rx_plain_buffer); + for(int i = 0; i < 4; i++) { + parity = (parity << 1) | (((parity_data[0] >> i) & 0x01) ^ 0x01); + } + + bool success = add_nested_nonce( + &dict_attack_ctx->nested_nonce, + cuid, + dict_attack_ctx->nested_target_key, + 0, + nt_enc, + parity, + 0); + if(!success) { + FURI_LOG_E(TAG, "Failed to add nested nonce to array. OOM?"); + dict_attack_ctx->auth_passed = false; + break; + } + + dict_attack_ctx->auth_passed = true; + } + // If we have sufficient nonces, search the dictionaries for the key + if((is_weak && (dict_attack_ctx->nested_nonce.count == 1)) || + (is_last_iter_for_hard_key && (dict_attack_ctx->nested_nonce.count == 8))) { + // Identify key candidates + MfClassicKey* key_candidate = search_dicts_for_nonce_key( + dict_attack_ctx, + &dict_attack_ctx->nested_nonce, + dict_attack_ctx->mf_classic_system_dict, + dict_attack_ctx->mf_classic_user_dict, + is_weak); + if(key_candidate != NULL) { + FURI_LOG_I( + TAG, + "Found key candidate %06llx", + bit_lib_bytes_to_num_be(key_candidate->data, sizeof(MfClassicKey))); + dict_attack_ctx->current_key = *key_candidate; + dict_attack_ctx->reuse_key_sector = target_sector; + dict_attack_ctx->current_key_type = target_key_type; + free(key_candidate); + break; + } else { + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + } + + FURI_LOG_D( + TAG, + "Target: %u (key type %s, block %u) cuid: %08lx", + dict_attack_ctx->nested_target_key, + (target_key_type == MfClassicKeyTypeA) ? "A" : "B", + target_block, + cuid); + } while(false); + + instance->state = MfClassicPollerStateNestedController; + + mf_classic_poller_halt(instance); + return command; +} + +NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { + furi_assert(instance->mode_ctx.dict_attack_ctx.nested_nonce.count > 0); + furi_assert(instance->mode_ctx.dict_attack_ctx.nested_nonce.nonces); + + NfcCommand command = NfcCommandContinue; + bool params_saved = false; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + Storage* storage = furi_record_open(RECORD_STORAGE); + Stream* stream = buffered_file_stream_alloc(storage); + FuriString* temp_str = furi_string_alloc(); + bool weak_prng = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; + bool static_encrypted = dict_attack_ctx->static_encrypted; + + do { + if(weak_prng && (!(static_encrypted)) && (dict_attack_ctx->nested_nonce.count != 2)) { + FURI_LOG_E( + TAG, + "MfClassicPollerStateNestedLog expected 2 nonces, received %zu", + dict_attack_ctx->nested_nonce.count); + break; + } + + uint32_t nonce_pair_count = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak ? + 1 : + dict_attack_ctx->nested_nonce.count; + + if(!buffered_file_stream_open( + stream, MF_CLASSIC_NESTED_LOGS_FILE_PATH, FSAM_WRITE, FSOM_OPEN_APPEND)) + break; + + bool params_write_success = true; + for(size_t i = 0; i < nonce_pair_count; i++) { + MfClassicNestedNonce* nonce = &dict_attack_ctx->nested_nonce.nonces[i]; + // TODO FL-3926: Avoid repeating logic here + uint8_t nonce_sector = nonce->key_idx / (weak_prng ? 4 : 2); + MfClassicKeyType nonce_key_type = + (nonce->key_idx % (weak_prng ? 4 : 2) < (weak_prng ? 2 : 1)) ? MfClassicKeyTypeA : + MfClassicKeyTypeB; + furi_string_printf( + temp_str, + "Sec %d key %c cuid %08lx", + nonce_sector, + (nonce_key_type == MfClassicKeyTypeA) ? 'A' : 'B', + nonce->cuid); + for(uint8_t nt_idx = 0; nt_idx < ((weak_prng && (!(static_encrypted))) ? 2 : 1); + nt_idx++) { + if(nt_idx == 1) { + nonce = &dict_attack_ctx->nested_nonce.nonces[i + 1]; + } + furi_string_cat_printf( + temp_str, + " nt%u %08lx ks%u %08lx par%u ", + nt_idx, + nonce->nt, + nt_idx, + nonce->nt_enc ^ nonce->nt, + nt_idx); + for(uint8_t pb = 0; pb < 4; pb++) { + furi_string_cat_printf(temp_str, "%u", (nonce->par >> (3 - pb)) & 1); + } + } + if(dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) { + furi_string_cat_printf(temp_str, " dist %u\n", nonce->dist); + } else { + furi_string_cat_printf(temp_str, "\n"); + } + if(!stream_write_string(stream, temp_str)) { + params_write_success = false; + break; + } + } + if(!params_write_success) break; + + params_saved = true; + } while(false); + + furi_assert(params_saved); + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + furi_string_free(temp_str); + buffered_file_stream_close(stream); + stream_free(stream); + furi_record_close(RECORD_STORAGE); + instance->state = MfClassicPollerStateNestedController; + return command; +} + +bool mf_classic_nested_is_target_key_found(MfClassicPoller* instance, bool is_dict_attack) { + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + bool is_weak = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; + uint16_t nested_target_key = dict_attack_ctx->nested_target_key; + + MfClassicKeyType target_key_type; + uint8_t target_sector; + + if(is_dict_attack) { + target_key_type = (((is_weak) && ((nested_target_key % 2) == 0)) || + ((!is_weak) && ((nested_target_key % 16) < 8))) ? + MfClassicKeyTypeA : + MfClassicKeyTypeB; + target_sector = is_weak ? (nested_target_key / 2) : (nested_target_key / 16); + } else { + target_key_type = (((is_weak) && ((nested_target_key % 4) < 2)) || + ((!is_weak) && ((nested_target_key % 2) == 0))) ? + MfClassicKeyTypeA : + MfClassicKeyTypeB; + target_sector = is_weak ? (nested_target_key / 4) : (nested_target_key / 2); + } + + return mf_classic_is_key_found(instance->data, target_sector, target_key_type); +} + +bool is_valid_sum(uint16_t sum) { + for(size_t i = 0; i < 19; i++) { + if(sum == valid_sums[i]) { + return true; + } + } + return false; +} + +NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance) { + // This function guides the nested attack through its phases, and iterates over the target keys + NfcCommand command = mf_classic_poller_handle_data_update(instance); + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + bool initial_dict_attack_iter = false; + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseNone) { + dict_attack_ctx->auth_passed = true; + bool backdoor_present = (dict_attack_ctx->backdoor != MfClassicBackdoorNone); + if(!(backdoor_present)) { + for(uint8_t sector = 0; sector < instance->sectors_total; sector++) { + for(uint8_t key_type = 0; key_type < 2; key_type++) { + if(mf_classic_is_key_found(instance->data, sector, key_type)) { + dict_attack_ctx->nested_known_key = + mf_classic_get_key(instance->data, sector, key_type); + dict_attack_ctx->nested_known_key_sector = sector; + dict_attack_ctx->nested_known_key_type = key_type; + break; + } + } + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseAnalyzePRNG; + } else { + dict_attack_ctx->nested_known_key = dict_attack_ctx->current_key; + dict_attack_ctx->nested_known_key_sector = 0; + dict_attack_ctx->nested_known_key_type = MfClassicKeyTypeA; + dict_attack_ctx->prng_type = MfClassicPrngTypeWeak; + if(dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) { + dict_attack_ctx->static_encrypted = true; + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; + initial_dict_attack_iter = true; + } + } + // Identify PRNG type + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseAnalyzePRNG) { + if(dict_attack_ctx->nested_nonce.count < MF_CLASSIC_NESTED_ANALYZE_NT_COUNT) { + instance->state = MfClassicPollerStateNestedCollectNt; + return command; + } else if( + (dict_attack_ctx->nested_nonce.count == MF_CLASSIC_NESTED_ANALYZE_NT_COUNT) && + (dict_attack_ctx->prng_type == MfClassicPrngTypeUnknown)) { + instance->state = MfClassicPollerStateNestedAnalyzePRNG; + return command; + } else if(dict_attack_ctx->prng_type == MfClassicPrngTypeNoTag) { + FURI_LOG_E(TAG, "No tag detected"); + // Free nonce array + // TODO FL-3926: Consider using .count here + if(dict_attack_ctx->nested_nonce.nonces) { + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + instance->state = MfClassicPollerStateFail; + return command; + } + if(dict_attack_ctx->nested_nonce.nonces) { + // Free nonce array + // TODO FL-3926: Consider using .count here + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; + initial_dict_attack_iter = true; + } + // Accelerated nested dictionary attack + bool is_weak = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; + uint16_t dict_target_key_max = (dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) ? + (instance->sectors_total * 2) : + (instance->sectors_total * 16); + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackVerify) { + if(!(mf_classic_nested_is_target_key_found(instance, true)) && + (dict_attack_ctx->nested_nonce.count > 0)) { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackResume; + instance->state = MfClassicPollerStateNestedDictAttack; + return command; + } else { + dict_attack_ctx->auth_passed = true; + if(dict_attack_ctx->nested_nonce.count > 0) { + // Free nonce array + furi_assert(dict_attack_ctx->nested_nonce.nonces); + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; + } + } + if((dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttack || + dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackResume) && + (dict_attack_ctx->nested_target_key < dict_target_key_max)) { + bool is_last_iter_for_hard_key = + ((!is_weak) && ((dict_attack_ctx->nested_target_key % 8) == 7)); + if(initial_dict_attack_iter) { + // Initialize dictionaries + // Note: System dict should always exist + dict_attack_ctx->mf_classic_system_dict = + keys_dict_check_presence(MF_CLASSIC_NESTED_SYSTEM_DICT_PATH) ? + keys_dict_alloc( + MF_CLASSIC_NESTED_SYSTEM_DICT_PATH, + KeysDictModeOpenExisting, + sizeof(MfClassicKey)) : + NULL; + + dict_attack_ctx->mf_classic_user_dict = + keys_dict_check_presence(MF_CLASSIC_NESTED_USER_DICT_PATH) ? + keys_dict_alloc( + MF_CLASSIC_NESTED_USER_DICT_PATH, + KeysDictModeOpenExisting, + sizeof(MfClassicKey)) : + NULL; + } + if((is_weak || is_last_iter_for_hard_key) && dict_attack_ctx->nested_nonce.count > 0) { + // Key verify and reuse + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackVerify; + dict_attack_ctx->auth_passed = false; + instance->state = MfClassicPollerStateKeyReuseStartNoOffset; + return command; + } else if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackResume) { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; + dict_attack_ctx->auth_passed = true; + } + if(!(dict_attack_ctx->auth_passed)) { + dict_attack_ctx->attempt_count++; + } else if(!(initial_dict_attack_iter)) { + dict_attack_ctx->nested_target_key++; + dict_attack_ctx->attempt_count = 0; + } + dict_attack_ctx->auth_passed = true; + if(dict_attack_ctx->nested_target_key == dict_target_key_max) { + if(dict_attack_ctx->mf_classic_system_dict) { + keys_dict_free(dict_attack_ctx->mf_classic_system_dict); + dict_attack_ctx->mf_classic_system_dict = NULL; + } + if(dict_attack_ctx->mf_classic_user_dict) { + keys_dict_free(dict_attack_ctx->mf_classic_user_dict); + dict_attack_ctx->mf_classic_user_dict = NULL; + } + dict_attack_ctx->nested_target_key = 0; + if(mf_classic_is_card_read(instance->data)) { + // All keys have been collected + FURI_LOG_D(TAG, "All keys collected and sectors read"); + dict_attack_ctx->nested_phase = MfClassicNestedPhaseFinished; + instance->state = MfClassicPollerStateSuccess; + return command; + } + if(dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) { + // Skip initial calibration for static encrypted backdoored tags + dict_attack_ctx->calibrated = true; + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseCalibrate; + instance->state = MfClassicPollerStateNestedController; + return command; + } + if(dict_attack_ctx->attempt_count == 0) { + // Check if the nested target key is a known key + if(mf_classic_nested_is_target_key_found(instance, true)) { + // Continue to next key + instance->state = MfClassicPollerStateNestedController; + return command; + } + } + if(dict_attack_ctx->attempt_count >= 3) { + // Unpredictable, skip + FURI_LOG_E(TAG, "Failed to collect nonce, skipping key"); + dict_attack_ctx->nested_target_key++; + dict_attack_ctx->attempt_count = 0; + } + instance->state = MfClassicPollerStateNestedDictAttack; + return command; + } + // Calibration + bool initial_collect_nt_enc_iter = false; + bool recalibrated = false; + if(!(dict_attack_ctx->calibrated)) { + if(dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) { + instance->state = MfClassicPollerStateNestedCalibrate; + return command; + } + initial_collect_nt_enc_iter = true; + dict_attack_ctx->calibrated = true; + dict_attack_ctx->nested_phase = MfClassicNestedPhaseCollectNtEnc; + } else if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseCalibrate) { + initial_collect_nt_enc_iter = true; + dict_attack_ctx->nested_phase = MfClassicNestedPhaseCollectNtEnc; + } else if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseRecalibrate) { + recalibrated = true; + dict_attack_ctx->nested_phase = MfClassicNestedPhaseCollectNtEnc; + } + // Collect and log nonces + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseCollectNtEnc) { + if(((is_weak) && (dict_attack_ctx->nested_nonce.count == 2)) || + ((is_weak) && (dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) && + (dict_attack_ctx->nested_nonce.count == 1)) || + ((!(is_weak)) && (dict_attack_ctx->nested_nonce.count > 0))) { + instance->state = MfClassicPollerStateNestedLog; + return command; + } + uint16_t nonce_collect_key_max; + if(dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) { + nonce_collect_key_max = instance->sectors_total * 4; + } else { + nonce_collect_key_max = instance->sectors_total * 2; + } + // Target all remaining sectors, key A and B + if(dict_attack_ctx->nested_target_key < nonce_collect_key_max) { + if((!(is_weak)) && (dict_attack_ctx->msb_count == (UINT8_MAX + 1))) { + if(is_valid_sum(dict_attack_ctx->msb_par_sum)) { + // All Hardnested nonces collected + dict_attack_ctx->nested_target_key++; + dict_attack_ctx->current_key_checked = false; + instance->state = MfClassicPollerStateNestedController; + } else { + // Nonces do not match an expected sum + dict_attack_ctx->attempt_count++; + instance->state = MfClassicPollerStateNestedCollectNtEnc; + } + dict_attack_ctx->msb_count = 0; + dict_attack_ctx->msb_par_sum = 0; + memset(dict_attack_ctx->nt_enc_msb, 0, sizeof(dict_attack_ctx->nt_enc_msb)); + return command; + } + if(initial_collect_nt_enc_iter) { + dict_attack_ctx->current_key_checked = false; + } + if(!(dict_attack_ctx->auth_passed) && !(initial_collect_nt_enc_iter)) { + dict_attack_ctx->attempt_count++; + } else { + if(is_weak && !(initial_collect_nt_enc_iter) && !(recalibrated)) { + if(!(dict_attack_ctx->static_encrypted)) { + dict_attack_ctx->nested_target_key++; + } else { + dict_attack_ctx->nested_target_key += 2; + } + if(dict_attack_ctx->nested_target_key % 2 == 0) { + dict_attack_ctx->current_key_checked = false; + } + } + dict_attack_ctx->attempt_count = 0; + } + dict_attack_ctx->auth_passed = true; + + // If we have tried to collect this nonce too many times, skip + if((is_weak && (dict_attack_ctx->attempt_count >= MF_CLASSIC_NESTED_RETRY_MAXIMUM)) || + (!(is_weak) && + (dict_attack_ctx->attempt_count >= MF_CLASSIC_NESTED_HARD_RETRY_MAXIMUM))) { + // Unpredictable, skip + FURI_LOG_W(TAG, "Failed to collect nonce, skipping key"); + if(dict_attack_ctx->nested_nonce.nonces) { + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + if(is_weak) { + dict_attack_ctx->nested_target_key += 2; + dict_attack_ctx->current_key_checked = false; + } else { + dict_attack_ctx->msb_count = 0; + dict_attack_ctx->msb_par_sum = 0; + memset(dict_attack_ctx->nt_enc_msb, 0, sizeof(dict_attack_ctx->nt_enc_msb)); + dict_attack_ctx->nested_target_key++; + dict_attack_ctx->current_key_checked = false; + } + dict_attack_ctx->attempt_count = 0; + } + + FURI_LOG_D( + TAG, + "Nested target key: %u (max: %u)", + dict_attack_ctx->nested_target_key, + nonce_collect_key_max); + + if(!(dict_attack_ctx->current_key_checked)) { + if(dict_attack_ctx->nested_target_key == nonce_collect_key_max) { + // All nonces have been collected + FURI_LOG_D(TAG, "All nonces collected"); + instance->state = MfClassicPollerStateNestedController; + return command; + } + + dict_attack_ctx->current_key_checked = true; + + // Check if the nested target key is a known key + if(mf_classic_nested_is_target_key_found(instance, false)) { + // Continue to next key + if(!(dict_attack_ctx->static_encrypted)) { + dict_attack_ctx->nested_target_key++; + dict_attack_ctx->current_key_checked = false; + } + instance->state = MfClassicPollerStateNestedController; + return command; + } + + // If it is not a known key, we'll need to calibrate for static encrypted backdoored tags + if((dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) && + (dict_attack_ctx->nested_target_key < nonce_collect_key_max) && + !(recalibrated)) { + dict_attack_ctx->calibrated = false; + dict_attack_ctx->nested_phase = MfClassicNestedPhaseRecalibrate; + instance->state = MfClassicPollerStateNestedController; + return command; + } + } + FURI_LOG_T(TAG, "Collecting a nonce"); + + // Collect a nonce + dict_attack_ctx->auth_passed = false; + instance->state = MfClassicPollerStateNestedCollectNtEnc; + return command; + } + } + dict_attack_ctx->nested_target_key = 0; + dict_attack_ctx->nested_phase = MfClassicNestedPhaseFinished; + instance->state = MfClassicPollerStateSuccess; + return command; +} + NfcCommand mf_classic_poller_handler_success(MfClassicPoller* instance) { NfcCommand command = NfcCommandContinue; instance->mfc_event.type = MfClassicPollerEventTypeSuccess; @@ -857,6 +2154,8 @@ static const MfClassicPollerReadHandler [MfClassicPollerStateWriteBlock] = mf_classic_poller_handler_write_block, [MfClassicPollerStateWriteValueBlock] = mf_classic_poller_handler_write_value_block, [MfClassicPollerStateNextSector] = mf_classic_poller_handler_next_sector, + [MfClassicPollerStateAnalyzeBackdoor] = mf_classic_poller_handler_analyze_backdoor, + [MfClassicPollerStateBackdoorReadSector] = mf_classic_poller_handler_backdoor_read_sector, [MfClassicPollerStateRequestKey] = mf_classic_poller_handler_request_key, [MfClassicPollerStateRequestReadSector] = mf_classic_poller_handler_request_read_sector, [MfClassicPollerStateReadSectorBlocks] = @@ -865,9 +2164,18 @@ static const MfClassicPollerReadHandler [MfClassicPollerStateAuthKeyB] = mf_classic_poller_handler_auth_b, [MfClassicPollerStateReadSector] = mf_classic_poller_handler_read_sector, [MfClassicPollerStateKeyReuseStart] = mf_classic_poller_handler_key_reuse_start, + [MfClassicPollerStateKeyReuseStartNoOffset] = + mf_classic_poller_handler_key_reuse_start_no_offset, [MfClassicPollerStateKeyReuseAuthKeyA] = mf_classic_poller_handler_key_reuse_auth_key_a, [MfClassicPollerStateKeyReuseAuthKeyB] = mf_classic_poller_handler_key_reuse_auth_key_b, [MfClassicPollerStateKeyReuseReadSector] = mf_classic_poller_handler_key_reuse_read_sector, + [MfClassicPollerStateNestedAnalyzePRNG] = mf_classic_poller_handler_nested_analyze_prng, + [MfClassicPollerStateNestedCalibrate] = mf_classic_poller_handler_nested_calibrate, + [MfClassicPollerStateNestedCollectNt] = mf_classic_poller_handler_nested_collect_nt, + [MfClassicPollerStateNestedController] = mf_classic_poller_handler_nested_controller, + [MfClassicPollerStateNestedCollectNtEnc] = mf_classic_poller_handler_nested_collect_nt_enc, + [MfClassicPollerStateNestedDictAttack] = mf_classic_poller_handler_nested_dict_attack, + [MfClassicPollerStateNestedLog] = mf_classic_poller_handler_nested_log, [MfClassicPollerStateSuccess] = mf_classic_poller_handler_success, [MfClassicPollerStateFail] = mf_classic_poller_handler_fail, }; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.h b/lib/nfc/protocols/mf_classic/mf_classic_poller.h index 518d029d078..8efb931aae7 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.h @@ -44,9 +44,46 @@ typedef enum { typedef enum { MfClassicPollerModeRead, /**< Poller reading mode. */ MfClassicPollerModeWrite, /**< Poller writing mode. */ - MfClassicPollerModeDictAttack, /**< Poller dictionary attack mode. */ + MfClassicPollerModeDictAttackStandard, /**< Poller dictionary attack mode. */ + MfClassicPollerModeDictAttackEnhanced, /**< Poller enhanced dictionary attack mode. */ } MfClassicPollerMode; +/** + * @brief MfClassic poller nested attack phase. + */ +typedef enum { + MfClassicNestedPhaseNone, /**< No nested attack has taken place yet. */ + MfClassicNestedPhaseAnalyzePRNG, /**< Analyze nonces produced by the PRNG to determine if they fit a weak PRNG */ + MfClassicNestedPhaseDictAttack, /**< Search keys which match the expected PRNG properties and parity for collected nonces */ + MfClassicNestedPhaseDictAttackVerify, /**< Verify candidate keys by authenticating to the sector with the key */ + MfClassicNestedPhaseDictAttackResume, /**< Resume nested dictionary attack from the last tested (invalid) key */ + MfClassicNestedPhaseCalibrate, /**< Perform necessary calculations to recover the plaintext nonce during later collection phase (weak PRNG tags only) */ + MfClassicNestedPhaseRecalibrate, /**< Collect the next plaintext static encrypted nonce for backdoor static encrypted nonce nested attack */ + MfClassicNestedPhaseCollectNtEnc, /**< Log nonces collected during nested authentication for key recovery */ + MfClassicNestedPhaseFinished, /**< Nested attack has finished */ +} MfClassicNestedPhase; + +/** + * @brief MfClassic pseudorandom number generator (PRNG) type. + */ +typedef enum { + MfClassicPrngTypeUnknown, // Tag not yet tested + MfClassicPrngTypeNoTag, // No tag detected during test + MfClassicPrngTypeWeak, // Weak PRNG, standard Nested + MfClassicPrngTypeHard, // Hard PRNG, Hardnested +} MfClassicPrngType; + +/** + * @brief MfClassic authentication backdoor type. + */ +typedef enum { + MfClassicBackdoorUnknown, // Tag not yet tested + MfClassicBackdoorNone, // No observed backdoor + MfClassicBackdoorAuth1, // Tag responds to v1 auth backdoor + MfClassicBackdoorAuth2, // Tag responds to v2 auth backdoor (sometimes static encrypted) + MfClassicBackdoorAuth3, // Tag responds to v3 auth backdoor (static encrypted nonce) +} MfClassicBackdoor; + /** * @brief MfClassic poller request mode event data. * @@ -77,6 +114,12 @@ typedef struct { uint8_t sectors_read; /**< Number of sectors read. */ uint8_t keys_found; /**< Number of keys found. */ uint8_t current_sector; /**< Current sector number. */ + MfClassicNestedPhase nested_phase; /**< Nested attack phase. */ + MfClassicPrngType prng_type; /**< PRNG (weak or hard). */ + MfClassicBackdoor backdoor; /**< Backdoor type. */ + uint16_t nested_target_key; /**< Target key for nested attack. */ + uint16_t + msb_count; /**< Number of unique most significant bytes seen during Hardnested attack. */ } MfClassicPollerEventDataUpdate; /** @@ -170,13 +213,15 @@ typedef struct { * @param[in] block_num block number for authentication. * @param[in] key_type key type to be used for authentication. * @param[out] nt pointer to the MfClassicNt structure to be filled with nonce data. + * @param[in] backdoor_auth flag indicating if backdoor authentication is used. * @return MfClassicErrorNone on success, an error code on failure. */ MfClassicError mf_classic_poller_get_nt( MfClassicPoller* instance, uint8_t block_num, MfClassicKeyType key_type, - MfClassicNt* nt); + MfClassicNt* nt, + bool backdoor_auth); /** * @brief Collect tag nonce during nested authentication. @@ -189,13 +234,15 @@ MfClassicError mf_classic_poller_get_nt( * @param[in] block_num block number for authentication. * @param[in] key_type key type to be used for authentication. * @param[out] nt pointer to the MfClassicNt structure to be filled with nonce data. + * @param[in] backdoor_auth flag indicating if backdoor authentication is used. * @return MfClassicErrorNone on success, an error code on failure. */ MfClassicError mf_classic_poller_get_nt_nested( MfClassicPoller* instance, uint8_t block_num, MfClassicKeyType key_type, - MfClassicNt* nt); + MfClassicNt* nt, + bool backdoor_auth); /** * @brief Perform authentication. @@ -210,6 +257,7 @@ MfClassicError mf_classic_poller_get_nt_nested( * @param[in] key key to be used for authentication. * @param[in] key_type key type to be used for authentication. * @param[out] data pointer to MfClassicAuthContext structure to be filled with authentication data. + * @param[in] backdoor_auth flag indicating if backdoor authentication is used. * @return MfClassicErrorNone on success, an error code on failure. */ MfClassicError mf_classic_poller_auth( @@ -217,20 +265,23 @@ MfClassicError mf_classic_poller_auth( uint8_t block_num, MfClassicKey* key, MfClassicKeyType key_type, - MfClassicAuthContext* data); + MfClassicAuthContext* data, + bool backdoor_auth); /** * @brief Perform nested authentication. * * Must ONLY be used inside the callback function. * - * Perform nested authentication as specified in Mf Classic protocol. + * Perform nested authentication as specified in Mf Classic protocol. * * @param[in, out] instance pointer to the instance to be used in the transaction. * @param[in] block_num block number for authentication. * @param[in] key key to be used for authentication. * @param[in] key_type key type to be used for authentication. * @param[out] data pointer to MfClassicAuthContext structure to be filled with authentication data. + * @param[in] backdoor_auth flag indicating if backdoor authentication is used. + * @param[in] early_ret return immediately after receiving encrypted nonce. * @return MfClassicErrorNone on success, an error code on failure. */ MfClassicError mf_classic_poller_auth_nested( @@ -238,7 +289,9 @@ MfClassicError mf_classic_poller_auth_nested( uint8_t block_num, MfClassicKey* key, MfClassicKeyType key_type, - MfClassicAuthContext* data); + MfClassicAuthContext* data, + bool backdoor_auth, + bool early_ret); /** * @brief Halt the tag. diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c index 949ef8e66ee..deccdbcdad3 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c @@ -5,7 +5,7 @@ #include -#define TAG "MfCLassicPoller" +#define TAG "MfClassicPoller" MfClassicError mf_classic_process_error(Iso14443_3aError error) { MfClassicError ret = MfClassicErrorNone; @@ -38,13 +38,20 @@ static MfClassicError mf_classic_poller_get_nt_common( uint8_t block_num, MfClassicKeyType key_type, MfClassicNt* nt, - bool is_nested) { + bool is_nested, + bool backdoor_auth) { MfClassicError ret = MfClassicErrorNone; Iso14443_3aError error = Iso14443_3aErrorNone; do { - uint8_t auth_type = (key_type == MfClassicKeyTypeB) ? MF_CLASSIC_CMD_AUTH_KEY_B : - MF_CLASSIC_CMD_AUTH_KEY_A; + uint8_t auth_type; + if(!backdoor_auth) { + auth_type = (key_type == MfClassicKeyTypeB) ? MF_CLASSIC_CMD_AUTH_KEY_B : + MF_CLASSIC_CMD_AUTH_KEY_A; + } else { + auth_type = (key_type == MfClassicKeyTypeB) ? MF_CLASSIC_CMD_BACKDOOR_AUTH_KEY_B : + MF_CLASSIC_CMD_BACKDOOR_AUTH_KEY_A; + } uint8_t auth_cmd[2] = {auth_type, block_num}; bit_buffer_copy_bytes(instance->tx_plain_buffer, auth_cmd, sizeof(auth_cmd)); @@ -89,29 +96,34 @@ MfClassicError mf_classic_poller_get_nt( MfClassicPoller* instance, uint8_t block_num, MfClassicKeyType key_type, - MfClassicNt* nt) { + MfClassicNt* nt, + bool backdoor_auth) { furi_check(instance); - return mf_classic_poller_get_nt_common(instance, block_num, key_type, nt, false); + return mf_classic_poller_get_nt_common( + instance, block_num, key_type, nt, false, backdoor_auth); } MfClassicError mf_classic_poller_get_nt_nested( MfClassicPoller* instance, uint8_t block_num, MfClassicKeyType key_type, - MfClassicNt* nt) { + MfClassicNt* nt, + bool backdoor_auth) { furi_check(instance); - return mf_classic_poller_get_nt_common(instance, block_num, key_type, nt, true); + return mf_classic_poller_get_nt_common(instance, block_num, key_type, nt, true, backdoor_auth); } -static MfClassicError mf_classic_poller_auth_common( +MfClassicError mf_classic_poller_auth_common( MfClassicPoller* instance, uint8_t block_num, MfClassicKey* key, MfClassicKeyType key_type, MfClassicAuthContext* data, - bool is_nested) { + bool is_nested, + bool backdoor_auth, + bool early_ret) { MfClassicError ret = MfClassicErrorNone; Iso14443_3aError error = Iso14443_3aErrorNone; @@ -122,14 +134,16 @@ static MfClassicError mf_classic_poller_auth_common( MfClassicNt nt = {}; if(is_nested) { - ret = mf_classic_poller_get_nt_nested(instance, block_num, key_type, &nt); + ret = + mf_classic_poller_get_nt_nested(instance, block_num, key_type, &nt, backdoor_auth); } else { - ret = mf_classic_poller_get_nt(instance, block_num, key_type, &nt); + ret = mf_classic_poller_get_nt(instance, block_num, key_type, &nt, backdoor_auth); } if(ret != MfClassicErrorNone) break; if(data) { data->nt = nt; } + if(early_ret) break; uint32_t cuid = iso14443_3a_get_cuid(instance->data->iso14443_3a_data); uint64_t key_num = bit_lib_bytes_to_num_be(key->data, sizeof(MfClassicKey)); @@ -182,10 +196,12 @@ MfClassicError mf_classic_poller_auth( uint8_t block_num, MfClassicKey* key, MfClassicKeyType key_type, - MfClassicAuthContext* data) { + MfClassicAuthContext* data, + bool backdoor_auth) { furi_check(instance); furi_check(key); - return mf_classic_poller_auth_common(instance, block_num, key, key_type, data, false); + return mf_classic_poller_auth_common( + instance, block_num, key, key_type, data, false, backdoor_auth, false); } MfClassicError mf_classic_poller_auth_nested( @@ -193,10 +209,13 @@ MfClassicError mf_classic_poller_auth_nested( uint8_t block_num, MfClassicKey* key, MfClassicKeyType key_type, - MfClassicAuthContext* data) { + MfClassicAuthContext* data, + bool backdoor_auth, + bool early_ret) { furi_check(instance); furi_check(key); - return mf_classic_poller_auth_common(instance, block_num, key, key_type, data, true); + return mf_classic_poller_auth_common( + instance, block_num, key, key_type, data, true, backdoor_auth, early_ret); } MfClassicError mf_classic_poller_halt(MfClassicPoller* instance) { diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h index 14a7c61fd47..915c899c3e7 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h @@ -3,13 +3,40 @@ #include "mf_classic_poller.h" #include #include +#include #include +#include +#include +#include +#include #ifdef __cplusplus extern "C" { #endif -#define MF_CLASSIC_FWT_FC (60000) +#define MF_CLASSIC_FWT_FC (60000) +#define NFC_FOLDER EXT_PATH("nfc") +#define NFC_ASSETS_FOLDER EXT_PATH("nfc/assets") +#define MF_CLASSIC_NESTED_ANALYZE_NT_COUNT (5) +#define MF_CLASSIC_NESTED_NT_HARD_MINIMUM (3) +#define MF_CLASSIC_NESTED_RETRY_MAXIMUM (60) +#define MF_CLASSIC_NESTED_HARD_RETRY_MAXIMUM (3) +#define MF_CLASSIC_NESTED_CALIBRATION_COUNT (21) +#define MF_CLASSIC_NESTED_LOGS_FILE_NAME ".nested.log" +#define MF_CLASSIC_NESTED_SYSTEM_DICT_FILE_NAME "mf_classic_dict_nested.nfc" +#define MF_CLASSIC_NESTED_USER_DICT_FILE_NAME "mf_classic_dict_user_nested.nfc" +#define MF_CLASSIC_NESTED_LOGS_FILE_PATH (NFC_FOLDER "/" MF_CLASSIC_NESTED_LOGS_FILE_NAME) +#define MF_CLASSIC_NESTED_SYSTEM_DICT_PATH \ + (NFC_ASSETS_FOLDER "/" MF_CLASSIC_NESTED_SYSTEM_DICT_FILE_NAME) +#define MF_CLASSIC_NESTED_USER_DICT_PATH \ + (NFC_ASSETS_FOLDER "/" MF_CLASSIC_NESTED_USER_DICT_FILE_NAME) +#define SET_PACKED_BIT(arr, bit) ((arr)[(bit) / 8] |= (1 << ((bit) % 8))) +#define GET_PACKED_BIT(arr, bit) ((arr)[(bit) / 8] & (1 << ((bit) % 8))) + +extern const MfClassicKey auth1_backdoor_key; +extern const MfClassicKey auth2_backdoor_key; +extern const MfClassicKey auth3_backdoor_key; +extern const uint16_t valid_sums[19]; typedef enum { MfClassicAuthStateIdle, @@ -21,6 +48,28 @@ typedef enum { MfClassicCardStateLost, } MfClassicCardState; +typedef struct { + MfClassicKey key; + MfClassicBackdoor type; +} MfClassicBackdoorKeyPair; + +extern const MfClassicBackdoorKeyPair mf_classic_backdoor_keys[]; +extern const size_t mf_classic_backdoor_keys_count; + +typedef struct { + uint32_t cuid; // Card UID + uint8_t key_idx; // Key index + uint32_t nt; // Nonce + uint32_t nt_enc; // Encrypted nonce + uint8_t par; // Parity + uint16_t dist; // Distance +} MfClassicNestedNonce; + +typedef struct { + MfClassicNestedNonce* nonces; + size_t count; +} MfClassicNestedNonceArray; + typedef enum { MfClassicPollerStateDetectType, MfClassicPollerStateStart, @@ -38,17 +87,29 @@ typedef enum { // Dict attack states MfClassicPollerStateNextSector, + MfClassicPollerStateAnalyzeBackdoor, + MfClassicPollerStateBackdoorReadSector, MfClassicPollerStateRequestKey, MfClassicPollerStateReadSector, MfClassicPollerStateAuthKeyA, MfClassicPollerStateAuthKeyB, MfClassicPollerStateKeyReuseStart, + MfClassicPollerStateKeyReuseStartNoOffset, MfClassicPollerStateKeyReuseAuthKeyA, MfClassicPollerStateKeyReuseAuthKeyB, MfClassicPollerStateKeyReuseReadSector, MfClassicPollerStateSuccess, MfClassicPollerStateFail, + // Enhanced dictionary attack states + MfClassicPollerStateNestedAnalyzePRNG, + MfClassicPollerStateNestedCalibrate, + MfClassicPollerStateNestedCollectNt, + MfClassicPollerStateNestedController, + MfClassicPollerStateNestedCollectNtEnc, + MfClassicPollerStateNestedDictAttack, + MfClassicPollerStateNestedLog, + MfClassicPollerStateNum, } MfClassicPollerState; @@ -70,6 +131,30 @@ typedef struct { bool auth_passed; uint16_t current_block; uint8_t reuse_key_sector; + MfClassicBackdoor backdoor; + // Enhanced dictionary attack and nested nonce collection + bool enhanced_dict; + MfClassicNestedPhase nested_phase; + MfClassicKey nested_known_key; + MfClassicKeyType nested_known_key_type; + bool current_key_checked; + uint8_t nested_known_key_sector; + uint16_t nested_target_key; + MfClassicNestedNonceArray nested_nonce; + MfClassicPrngType prng_type; + bool static_encrypted; + uint32_t static_encrypted_nonce; + bool calibrated; + uint16_t d_min; + uint16_t d_max; + uint8_t attempt_count; + KeysDict* mf_classic_system_dict; + KeysDict* mf_classic_user_dict; + // Hardnested + uint8_t nt_enc_msb + [32]; // Bit-packed array to track which unique most significant bytes have been seen (256 bits = 32 bytes) + uint16_t msb_par_sum; // Sum of parity bits for each unique most significant byte + uint16_t msb_count; // Number of unique most significant bytes seen } MfClassicPollerDictAttackContext; typedef struct { diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_sync.c b/lib/nfc/protocols/mf_classic/mf_classic_poller_sync.c index cc6bc090855..13b51786f6d 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_sync.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_sync.c @@ -37,7 +37,8 @@ static MfClassicError mf_classic_poller_collect_nt_handler( poller, data->collect_nt_context.block, data->collect_nt_context.key_type, - &data->collect_nt_context.nt); + &data->collect_nt_context.nt, + false); } static MfClassicError @@ -47,7 +48,8 @@ static MfClassicError data->auth_context.block_num, &data->auth_context.key, data->auth_context.key_type, - &data->auth_context); + &data->auth_context, + false); } static MfClassicError mf_classic_poller_read_block_handler( @@ -61,7 +63,8 @@ static MfClassicError mf_classic_poller_read_block_handler( data->read_block_context.block_num, &data->read_block_context.key, data->read_block_context.key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) break; error = mf_classic_poller_read_block( @@ -87,7 +90,8 @@ static MfClassicError mf_classic_poller_write_block_handler( data->read_block_context.block_num, &data->read_block_context.key, data->read_block_context.key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) break; error = mf_classic_poller_write_block( @@ -113,7 +117,8 @@ static MfClassicError mf_classic_poller_read_value_handler( data->read_value_context.block_num, &data->read_value_context.key, data->read_value_context.key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) break; MfClassicBlock block = {}; @@ -144,7 +149,8 @@ static MfClassicError mf_classic_poller_change_value_handler( data->change_value_context.block_num, &data->change_value_context.key, data->change_value_context.key_type, - NULL); + NULL, + false); if(error != MfClassicErrorNone) break; error = mf_classic_poller_value_cmd( diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c index 0e5ff001162..7d51f6c6ee8 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c @@ -505,16 +505,14 @@ static NfcCommand mf_ultralight_poller_handler_read_pages(MfUltralightPoller* in instance->error = mf_ultralight_poller_read_page(instance, start_page, &data); } - const uint8_t read_cnt = instance->data->type == MfUltralightTypeMfulC ? 1 : 4; if(instance->error == MfUltralightErrorNone) { - for(size_t i = 0; i < read_cnt; i++) { - if(start_page + i < instance->pages_total) { - FURI_LOG_D(TAG, "Read page %d success", start_page + i); - instance->data->page[start_page + i] = data.page[i]; - 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/lib/toolbox/bit_buffer.c b/lib/toolbox/bit_buffer.c index 85a52e79d60..e261e80d4fd 100644 --- a/lib/toolbox/bit_buffer.c +++ b/lib/toolbox/bit_buffer.c @@ -113,7 +113,7 @@ void bit_buffer_copy_bytes_with_parity(BitBuffer* buf, const uint8_t* data, size uint8_t bit = FURI_BIT(data[bits_processed / BITS_IN_BYTE + 1], bits_processed % BITS_IN_BYTE); - if(bits_processed % BITS_IN_BYTE) { + if((bits_processed % BITS_IN_BYTE) == 0) { buf->parity[curr_byte / BITS_IN_BYTE] = bit; } else { buf->parity[curr_byte / BITS_IN_BYTE] |= bit << (bits_processed % BITS_IN_BYTE); diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 7943c4cfcb7..b5d51a0dd5c 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.2,, +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 c121fc71681..ee81f76a97a 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.2,, +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,, @@ -890,10 +890,15 @@ Function,+,crypto1_alloc,Crypto1*, Function,+,crypto1_bit,uint8_t,"Crypto1*, uint8_t, int" Function,+,crypto1_byte,uint8_t,"Crypto1*, uint8_t, int" Function,+,crypto1_decrypt,void,"Crypto1*, const BitBuffer*, BitBuffer*" +Function,+,crypto1_decrypt_nt_enc,uint32_t,"uint32_t, uint32_t, MfClassicKey" Function,+,crypto1_encrypt,void,"Crypto1*, uint8_t*, const BitBuffer*, BitBuffer*" Function,+,crypto1_encrypt_reader_nonce,void,"Crypto1*, uint64_t, uint32_t, uint8_t*, uint8_t*, BitBuffer*, _Bool" Function,+,crypto1_free,void,Crypto1* Function,+,crypto1_init,void,"Crypto1*, uint64_t" +Function,+,crypto1_is_weak_prng_nonce,_Bool,uint32_t +Function,+,crypto1_lfsr_rollback_word,uint32_t,"Crypto1*, uint32_t, int" +Function,+,crypto1_nonce_matches_encrypted_parity_bits,_Bool,"uint32_t, uint32_t, uint8_t" +Function,+,crypto1_prng_successor,uint32_t,"uint32_t, uint32_t" Function,+,crypto1_reset,void,Crypto1* Function,+,crypto1_word,uint32_t,"Crypto1*, uint32_t, int" Function,-,ctermid,char*,char* @@ -1225,6 +1230,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*" @@ -1560,6 +1566,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, @@ -1577,8 +1584,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 @@ -2507,6 +2517,7 @@ Function,+,mf_classic_get_base_data,Iso14443_3aData*,const MfClassicData* Function,+,mf_classic_get_blocks_num_in_sector,uint8_t,uint8_t Function,+,mf_classic_get_device_name,const char*,"const MfClassicData*, NfcDeviceNameType" Function,+,mf_classic_get_first_block_num_of_sector,uint8_t,uint8_t +Function,+,mf_classic_get_key,MfClassicKey,"const MfClassicData*, uint8_t, MfClassicKeyType" Function,+,mf_classic_get_read_sectors_and_keys,void,"const MfClassicData*, uint8_t*, uint8_t*" Function,+,mf_classic_get_sector_by_block,uint8_t,uint8_t Function,+,mf_classic_get_sector_trailer_by_sector,MfClassicSectorTrailer*,"const MfClassicData*, uint8_t" @@ -2525,10 +2536,10 @@ Function,+,mf_classic_is_sector_read,_Bool,"const MfClassicData*, uint8_t" Function,+,mf_classic_is_sector_trailer,_Bool,uint8_t Function,+,mf_classic_is_value_block,_Bool,"MfClassicSectorTrailer*, uint8_t" Function,+,mf_classic_load,_Bool,"MfClassicData*, FlipperFormat*, uint32_t" -Function,+,mf_classic_poller_auth,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKey*, MfClassicKeyType, MfClassicAuthContext*" -Function,+,mf_classic_poller_auth_nested,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKey*, MfClassicKeyType, MfClassicAuthContext*" -Function,+,mf_classic_poller_get_nt,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKeyType, MfClassicNt*" -Function,+,mf_classic_poller_get_nt_nested,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKeyType, MfClassicNt*" +Function,+,mf_classic_poller_auth,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKey*, MfClassicKeyType, MfClassicAuthContext*, _Bool" +Function,+,mf_classic_poller_auth_nested,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKey*, MfClassicKeyType, MfClassicAuthContext*, _Bool, _Bool" +Function,+,mf_classic_poller_get_nt,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKeyType, MfClassicNt*, _Bool" +Function,+,mf_classic_poller_get_nt_nested,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicKeyType, MfClassicNt*, _Bool" Function,+,mf_classic_poller_halt,MfClassicError,MfClassicPoller* Function,+,mf_classic_poller_read_block,MfClassicError,"MfClassicPoller*, uint8_t, MfClassicBlock*" Function,+,mf_classic_poller_send_custom_parity_frame,MfClassicError,"MfClassicPoller*, const BitBuffer*, BitBuffer*, uint32_t" @@ -2824,6 +2835,7 @@ Function,+,nfc_set_mask_receive_time_fc,void,"Nfc*, uint32_t" Function,+,nfc_start,void,"Nfc*, NfcEventCallback, void*" Function,+,nfc_stop,void,Nfc* Function,+,nfc_util_even_parity32,uint8_t,uint32_t +Function,+,nfc_util_even_parity8,uint8_t,uint8_t Function,+,nfc_util_odd_parity,void,"const uint8_t*, uint8_t*, uint8_t" Function,+,nfc_util_odd_parity8,uint8_t,uint8_t Function,+,notification_internal_message,void,"NotificationApp*, const NotificationSequence*" @@ -2938,7 +2950,6 @@ Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" Function,-,printf,int,"const char*, ..." -Function,+,prng_successor,uint32_t,"uint32_t, uint32_t" Function,+,property_value_out,void,"PropertyValueContext*, const char*, unsigned int, ..." Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, size_t" Function,+,protocol_dict_decoders_feed,ProtocolId,"ProtocolDict*, _Bool, uint32_t" @@ -3967,6 +3978,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 cf10c8d33df..27872570e8e 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 2326d3c0acd..5ea830cedef 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 37c6a8b1bba..fe5c0cf1731 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 d5cda747675..ab592fb7879 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 c5eab12ec5f..9c48fac0b57 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 cf483553f1f..284bd558a31 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 2655a8b97b7..53f0a36253a 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