From bf7b91f45f684c58f573cf87483ebafa4a5df3be Mon Sep 17 00:00:00 2001 From: noproto Date: Wed, 23 Oct 2024 05:21:43 -0400 Subject: [PATCH 01/26] Fix typo --- lib/nfc/protocols/mf_classic/mf_classic_poller_i.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aac05748fd..deccdbcdad 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; From d0214c7b272d277fc5f3bded5e0d65adf1a63be1 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 23 Oct 2024 20:07:03 +0300 Subject: [PATCH 02/26] fix: incorrect usage of mjs_own --- applications/system/js_app/modules/js_gpio.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 70021968fa..d2d65da4d9 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -269,7 +269,6 @@ 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)); From e1fdc5e98492d4a3d1bafd9b7fb2ee2adc82ed3c Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 03:20:53 +0100 Subject: [PATCH 03/26] NFC: NDEF SmartPoster support (#275) * NFC: NDEF SmartPoster support * Consistent naming * Nicer result output * Update changelog --- CHANGELOG.md | 8 ++-- .../main/nfc/plugins/supported_cards/ndef.c | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f70030ec..2a78b29237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,9 +115,11 @@ - OFW: USB/BT Remote: Mouse clicker option to click as fast as possible (by @sumukhj1219) - CLI: Print plugin name on load fail (by @Willy-JL) - NFC: - - NDEF parser supports Mifare Classic (#265 by @luu176), rewritten to be protocol-agnostic and more improvements (#265 by @Willy-JL) - - NDEF parser supports NTAG I2C Plus 1k and 2k chips too (by @RocketGod-git) - - NDEF parser decodes URL-encoded URI characters (#267 by @jaylikesbunda) + - NDEF Parser: + - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) + - Decoding of URL-encoded URI characters (#267 by @jaylikesbunda) + - SmartPoster record support (#275 by @Willy-JL) + - Enable parsing NTAG I2C Plus 1k and 2k chips too (#237 by @RocketGod-git) - Added 6 new Mifare Classic keys from Bulgaria Hotel (#216 by @z3r0l1nk) - UL: Add iq aparts hotel key (by @xMasterX) - OFW/UL: Rename 'Detect Reader' to 'Extract MFC Keys' (by @bettse & @xMasterX) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index f2ef765a64..b4c56d64fa 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -492,7 +492,20 @@ static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { return true; } -static bool ndef_parse_payload( +// ---=== ndef layout parsing ===--- + +static bool + ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); +static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed); +static bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, + const char* type, + uint8_t type_len); + +static bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, @@ -507,13 +520,16 @@ static bool ndef_parse_payload( switch(tnf) { case NdefTnfWellKnownType: - if(strncmp("U", type, type_len) == 0) { + 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, "Unsupported\n"); + 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; @@ -527,7 +543,7 @@ static bool ndef_parse_payload( return ndef_parse_wifi(ndef, pos, len); } // Dump data without parsing - furi_string_cat(ndef->output, "Unsupported\n"); + 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; @@ -548,11 +564,10 @@ static bool ndef_parse_payload( } } -// ---=== tlv and message parsing ===--- - // NDEF message structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format -static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num) { +static 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; @@ -599,7 +614,7 @@ static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t messag } // Payload Type - char type_buf[32]; // Longest type supported in ndef_parse_payload() is 32 chars excl terminator + 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) { @@ -617,9 +632,12 @@ static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t messag // Payload ID pos += id_len; - furi_string_cat_printf(ndef->output, "\e*> M%dR%d: ", message_num, record_num); - if(!ndef_parse_payload( - ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { + if(smart_poster) { + furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", 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; } @@ -678,7 +696,8 @@ static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { break; } - if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed)) return 0; + if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed, false)) + return 0; pos += len; break; From 907a8403aedfbb52a0b3c4e8109c9f8c03d8ea61 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 03:28:14 +0100 Subject: [PATCH 04/26] Update apps - TOTP: Added polyfill for cli/cli_ansi.h (by akopachov) --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index 3700ee70a6..bb84d4d2f0 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 3700ee70a6b4a924bfb9bd3cb66f6985d1de40a7 +Subproject commit bb84d4d2f0d4eb57f5162d43518f0627cad9c0c0 From 56332919de9c352f0bc87064c08e127592bebe4e Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 03:32:34 +0100 Subject: [PATCH 05/26] Format --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index bb84d4d2f0..067aa52f52 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit bb84d4d2f0d4eb57f5162d43518f0627cad9c0c0 +Subproject commit 067aa52f521e3f05e2181f753bffab733c424602 From 96e5b24a71082a465d3abacf3084b08f2675356c Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:27:41 +0100 Subject: [PATCH 06/26] JS: Fourth batch of OFW PR changes --- .../examples/apps/Scripts/Examples/gui.js | 8 ++--- .../examples/apps/Scripts/interactive.js | 5 ++- applications/system/js_app/js_thread.c | 14 +++++--- .../system/js_app/modules/js_gui/byte_input.c | 32 ++++++++++++++----- .../system/js_app/modules/js_gui/js_gui.c | 4 +-- .../system/js_app/modules/js_gui/js_gui.h | 2 +- .../system/js_app/modules/js_gui/submenu.c | 4 +-- .../system/js_app/modules/js_gui/text_input.c | 17 +++++++++- applications/system/js_app/types/global.d.ts | 6 ++-- 9 files changed, 64 insertions(+), 28 deletions(-) diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js index 5faeb6d323..a1e023853e 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js @@ -17,18 +17,16 @@ let views = { empty: emptyView.make(), keyboard: textInputView.makeWith({ header: "Enter your name", - defaultText: flipper.getName(), - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 32, + defaultText: flipper.getName(), + defaultTextClear: true, }), helloDialog: dialogView.make(), bytekb: byteInputView.makeWith({ header: "Look ma, I'm a header text!", - defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), - // Props for makeWith() are passed in reverse order, so length must be after defaultData 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.", diff --git a/applications/system/js_app/examples/apps/Scripts/interactive.js b/applications/system/js_app/examples/apps/Scripts/interactive.js index 34639cdaca..40ca98c309 100644 --- a/applications/system/js_app/examples/apps/Scripts/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/interactive.js @@ -24,11 +24,10 @@ let views = { }), textInput: textInput.makeWith({ header: "Type JavaScript Code:", - defaultText: "2+2", - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 256, + defaultText: "2+2", + defaultTextClear: true, }), loading: loading.make(), }; diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 7a774d324b..83f9e604cd 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,12 +198,18 @@ static void js_require(struct mjs* mjs) { 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) == 1) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str)); - } else { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base)); + 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; diff --git a/applications/system/js_app/modules/js_gui/byte_input.c b/applications/system/js_app/modules/js_gui/byte_input.c index 5c8844d222..2d6dae4755 100644 --- a/applications/system/js_app/modules/js_gui/byte_input.c +++ b/applications/system/js_app/modules/js_gui/byte_input.c @@ -8,6 +8,7 @@ typedef struct { uint8_t* buffer; size_t buffer_size; + size_t default_data_size; FuriString* header; FuriSemaphore* input_semaphore; JsEventLoopContract contract; @@ -38,7 +39,14 @@ static bool len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) { UNUSED(mjs); UNUSED(input); - context->buffer_size = (size_t)(value.number); + 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, @@ -57,16 +65,24 @@ static bool default_data_assign( JsByteKbContext* context) { UNUSED(mjs); - mjs_val_t array_buf = value.array; + mjs_val_t array_buf = value.term; if(mjs_is_data_view(array_buf)) { array_buf = mjs_dataview_get_buf(mjs, array_buf); } - size_t default_data_len = 0; - char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &default_data_len); - memcpy( - context->buffer, - (uint8_t*)default_data, - MIN((size_t)context->buffer_size, default_data_len)); + 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, 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 4bd4ccc31b..22d04855d2 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -216,14 +216,14 @@ 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){.array = value}; + c_value = (JsViewPropValue){.term = value}; } break; case JsViewPropTypeBool: { if(!mjs_is_boolean(value)) { 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 67266b1fc3..d400d0a33c 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -16,8 +16,8 @@ typedef enum { 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 aecd413be4..c142bcddb6 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 d2bf4a8f93..e93bbfad0e 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -8,6 +8,7 @@ typedef struct { char* buffer; size_t buffer_size; + size_t default_text_size; FuriString* header; bool default_text_clear; FuriSemaphore* input_semaphore; @@ -49,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, @@ -70,6 +78,13 @@ static bool default_text_assign( 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); text_input_set_result_callback( input, diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index d132f89f58..a55dae7d96 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -31,8 +31,10 @@ declare const __filename: string; /** * @brief Reads a JS value from a file * - * Reads a file at the specified path, interprets it as a JS value and returns - * the last value pushed on the stack. + * Reads a file at the specified path and runs it as JS, returning the last evaluated value. + * + * The result is cached and this filepath will not re-evaluated on future + * load() calls for this session. * * @param path The path to the file * @param scope An object to use as global scope while running this file From 19a65a85ba6bb544579b4ebc0dfbe3c2179910e3 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:28:26 +0100 Subject: [PATCH 07/26] JS: Temp enable illegal symbols in textinput until prop is available --- applications/system/js_app/modules/js_gui/text_input.c | 1 + 1 file changed, 1 insertion(+) 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 e93bbfad0e..79a1383884 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -135,6 +135,7 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_ .transformer_context = context, }, }; + text_input_add_illegal_symbols(input); text_input_set_result_callback( input, (TextInputCallback)input_callback, From 6dbb46a81d1de7aa0bc55775461ef747a3ab6964 Mon Sep 17 00:00:00 2001 From: noproto Date: Fri, 25 Oct 2024 18:38:11 -0400 Subject: [PATCH 08/26] Fix issue with resume logic --- applications/main/nfc/views/dict_attack.c | 2 ++ .../protocols/mf_classic/mf_classic_poller.c | 13 +++++++++---- .../protocols/mf_classic/mf_classic_poller.h | 17 +++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/applications/main/nfc/views/dict_attack.c b/applications/main/nfc/views/dict_attack.c index 4efbe6d604..726076972f 100644 --- a/applications/main/nfc/views/dict_attack.c +++ b/applications/main/nfc/views/dict_attack.c @@ -45,6 +45,7 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { furi_string_set(m->header, "PRNG Analysis"); break; case MfClassicNestedPhaseDictAttack: + case MfClassicNestedPhaseDictAttackVerify: case MfClassicNestedPhaseDictAttackResume: furi_string_set(m->header, "Nested Dictionary"); break; @@ -91,6 +92,7 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { 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 = diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 8d58658652..edfdb2a959 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -1881,9 +1881,10 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance 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 == MfClassicNestedPhaseDictAttackResume) { + 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 { @@ -1898,7 +1899,8 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; } } - if((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)); @@ -1922,11 +1924,14 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance NULL; } if((is_weak || is_last_iter_for_hard_key) && dict_attack_ctx->nested_nonce.count > 0) { - // Key reuse - dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackResume; + // 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++; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.h b/lib/nfc/protocols/mf_classic/mf_classic_poller.h index 7dfd3b6ab1..8efb931aae 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.h @@ -52,14 +52,15 @@ typedef enum { * @brief MfClassic poller nested attack phase. */ typedef enum { - MfClassicNestedPhaseNone, - MfClassicNestedPhaseAnalyzePRNG, - MfClassicNestedPhaseDictAttack, - MfClassicNestedPhaseDictAttackResume, - MfClassicNestedPhaseCalibrate, - MfClassicNestedPhaseRecalibrate, - MfClassicNestedPhaseCollectNtEnc, - MfClassicNestedPhaseFinished, + 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; /** From a50ae0a6041e2915043b976876c68bf721b87727 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:48:33 +0100 Subject: [PATCH 09/26] NFC: NDEF Parser SLIX support (#278) * Simpler protocol check * Simplify header code * Minor code fixes * NFC: NDEF Parser SLIX support * Parse success even without TLV terminator * Also parse empty NDEF TLVs * Add NDEF_PROTO_RAW to feed data manually * Update changelog * Ordering * Comment changes * Fix inefficiency in MAD checking --- CHANGELOG.md | 1 + applications/main/nfc/application.fam | 10 + .../main/nfc/plugins/supported_cards/ndef.c | 207 ++++++++++++++---- 3 files changed, 171 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a78b29237..fdb4830ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ - NFC: - NDEF Parser: - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) + - SLIX support, parse even with TLV terminator omitted, parse empty NDEF TLVs (#277 by @Willy-JL) - Decoding of URL-encoded URI characters (#267 by @jaylikesbunda) - SmartPoster record support (#275 by @Willy-JL) - Enable parsing NTAG I2C Plus 1k and 2k chips too (#237 by @RocketGod-git) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index f49ba84a2d..53c04f609e 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -274,6 +274,16 @@ App( 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="itso_parser", apptype=FlipperAppType.PLUGIN, diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index b4c56d64fa..97e67e9ada 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -1,34 +1,49 @@ // 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 +// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty, SmartPoster // Documentation and sources indicated where relevant // Made by @Willy-JL -// Mifare Classic support added by @luu176 +// 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, skip blocks or sectors as needed. This way, -// NDEF parsing code does not need to know details of card layout. +// 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_NONE (0) -#define NDEF_PROTO_UL (1) -#define NDEF_PROTO_MFC (2) +#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) -#if !defined(NDEF_PROTO) || (NDEF_PROTO != NDEF_PROTO_UL && NDEF_PROTO != NDEF_PROTO_MFC) +#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 ===--- @@ -72,8 +87,7 @@ _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[] = { - // clang-format off - [0x00] = NULL, + [0x00] = NULL, // Allows detecting no prepend and checking schema for type [0x01] = "http://www.", [0x02] = "https://www.", [0x03] = "http://", @@ -109,7 +123,6 @@ static const char* ndef_uri_prepends[] = { [0x21] = "urn:epc:raw:", [0x22] = "urn:epc:", [0x23] = "urn:nfc:", - // clang-format on }; // ---=== card memory layout abstraction ===--- @@ -117,7 +130,12 @@ static const char* ndef_uri_prepends[] = { // Shared context and state, read above typedef struct { FuriString* output; -#if NDEF_PROTO == NDEF_PROTO_UL +#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; @@ -127,11 +145,23 @@ typedef 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_UL +#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; @@ -188,6 +218,13 @@ static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { 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); @@ -494,18 +531,17 @@ static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { // ---=== ndef layout parsing ===--- -static bool - ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); -static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed); -static bool ndef_parse_record( +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); -static bool ndef_parse_record( +bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, @@ -566,8 +602,7 @@ static bool ndef_parse_record( // NDEF message structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format -static bool - ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { +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; @@ -648,15 +683,24 @@ static bool furi_string_cat(ndef->output, "\n\n"); } - return pos == end && last_record; + 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%d: 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 -static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { +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(true) { + while(pos < end) { NdefTlv tlv; if(!ndef_get(ndef, pos++, 1, &tlv)) return 0; FURI_LOG_D(TAG, "tlv: %02X", tlv); @@ -704,8 +748,14 @@ static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { } } } + + // 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 @@ -727,11 +777,11 @@ static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { return false; } - // Double check Capability Container (CC) values + // Check Capability Container (CC) values struct { uint8_t nfc_magic_number; uint8_t document_version_number; - uint8_t data_area_size; + 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; @@ -739,24 +789,23 @@ static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { // Calculate usable data area const uint8_t* start = &data->page[4].data[0]; - const uint8_t* end = start + (cc->data_area_size * 2 * MF_ULTRALIGHT_PAGE_SIZE); + 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; - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); + NDEF_TITLE(device, parsed_data); Ndef ndef = { .output = parsed_data, .ul = { .start = start, - .size = end - start, + .size = data_size, }, }; - size_t parsed = ndef_parse_tlv(&ndef, 0, 0); + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); if(parsed) { furi_string_trim(parsed_data, "\n"); @@ -802,17 +851,17 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { {64, 23}, }; for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { - if(sector_count <= 16 && mad > 0) break; // Skip MAD2 if not present + 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 size_t block = mads[mad].block; - const size_t sector = mf_classic_get_sector_by_block(block); - // 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 const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; if(!memcmp(aid, ndef_aid, AID_SIZE)) { sectors_with_ndef[aid_index + 1] = true; @@ -820,10 +869,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { } } - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_classic_get_device_name(data, NfcDeviceNameTypeFull)); + 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. @@ -870,7 +916,8 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { data_block = 93 + (sector - 32) * 15; } FURI_LOG_D(TAG, "data_block: %d", data_block); - size_t parsed = ndef_parse_tlv(&ndef, data_block * MF_CLASSIC_BLOCK_SIZE, total_parsed); + 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; @@ -888,6 +935,67 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* 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: 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 ===--- @@ -902,6 +1010,9 @@ static const NfcSupportedCardsPlugin ndef_plugin = { #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 }; @@ -916,3 +1027,5 @@ static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { const FlipperAppPluginDescriptor* ndef_plugin_ep(void) { return &ndef_plugin_descriptor; } + +#endif From 170db80b6deb9bf749235ee9640726bf0d3db672 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:49:13 +0100 Subject: [PATCH 10/26] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb4830ac6..173720bd82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,7 +117,7 @@ - NFC: - NDEF Parser: - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) - - SLIX support, parse even with TLV terminator omitted, parse empty NDEF TLVs (#277 by @Willy-JL) + - SLIX support, parse even with TLV terminator omitted, parse empty NDEF TLVs (#278 by @Willy-JL) - Decoding of URL-encoded URI characters (#267 by @jaylikesbunda) - SmartPoster record support (#275 by @Willy-JL) - Enable parsing NTAG I2C Plus 1k and 2k chips too (#237 by @RocketGod-git) From ef2dd08b713e782218b05a34775161a892cafd9c Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:57:19 +0000 Subject: [PATCH 11/26] NFC: NDEF parser less RAM waste for contact vcards --- .../main/nfc/plugins/supported_cards/ndef.c | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index 97e67e9ada..f65e9f548f 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -392,33 +392,67 @@ static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { } static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { - size_t end = pos + len; - - // Same concept as ndef_dump(), inefficient but has least drawbacks - FuriString* fmt = furi_string_alloc(); - furi_string_reserve(fmt, len + 1); - while(pos < end) { - char c; - if(!ndef_get(ndef, pos++, 1, &c)) return false; - furi_string_push_back(fmt, c); + // 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 miniml 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; + } } - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "BEGIN:VCARD")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - if(furi_string_end_with(fmt, "END:VCARD")) { - furi_string_left(fmt, furi_string_search_rchar(fmt, '\n')); + // 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; } - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "VERSION:")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - furi_string_trim(fmt); + } + + // 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_print(ndef, NULL, furi_string_get_cstr(fmt), furi_string_size(fmt), false); - furi_string_free(fmt); + ndef_dump(ndef, NULL, pos, len, false); + return true; } @@ -863,7 +897,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { // 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)) { + if(memcmp(aid, ndef_aid, AID_SIZE) == 0) { sectors_with_ndef[aid_index + 1] = true; } } From 2788ae14a6c43a907922417549232931fd536cb6 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:59:39 +0000 Subject: [PATCH 12/26] Fix typo --- applications/main/nfc/plugins/supported_cards/ndef.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index f65e9f548f..820324421d 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -397,7 +397,7 @@ static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { // 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 miniml stack and no heap usage at all. + // 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:"; From 3f1b181c5326dd154f67df0c027f610190e9c3c5 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:21:54 +0000 Subject: [PATCH 13/26] Asset Packer: Enforce LF when packing --nobuild --- scripts/asset_packer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/asset_packer.py b/scripts/asset_packer.py index dcfa68f8ee..095b899707 100755 --- a/scripts/asset_packer.py +++ b/scripts/asset_packer.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# https://github.com/Next-Flip/Momentum-Firmware/blob/dev/scripts/asset_packer.py from PIL import Image, ImageOps import heatshrink2 import pathlib @@ -45,6 +45,10 @@ def convert_bmx(img: "Image.Image | pathlib.Path") -> bytes: return data +def copy_file_as_lf(src: "pathlib.Path", dst: "pathlib.Path"): + dst.write_bytes(src.read_bytes().replace(b"\r\n", b"\n")) + + def pack_anim(src: pathlib.Path, dst: pathlib.Path): if not (src / "meta.txt").is_file(): return @@ -53,7 +57,7 @@ def pack_anim(src: pathlib.Path, dst: pathlib.Path): if not frame.is_file(): continue if frame.name == "meta.txt": - shutil.copyfile(frame, dst / frame.name) + copy_file_as_lf(frame, dst / frame.name) elif frame.name.startswith("frame_"): if frame.suffix == ".png": (dst / frame.with_suffix(".bm").name).write_bytes(convert_bm(frame)) @@ -145,7 +149,7 @@ def pack( if (source / "Anims/manifest.txt").exists(): (packed / "Anims").mkdir(parents=True, exist_ok=True) - shutil.copyfile( + copy_file_as_lf( source / "Anims/manifest.txt", packed / "Anims/manifest.txt" ) manifest = (source / "Anims/manifest.txt").read_bytes() From e00d1aee8f4b0e3f746b39338c07c106d8b57222 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:22:54 +0000 Subject: [PATCH 14/26] Oops --nobuild --- scripts/asset_packer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/asset_packer.py b/scripts/asset_packer.py index 095b899707..dda625fc32 100755 --- a/scripts/asset_packer.py +++ b/scripts/asset_packer.py @@ -1,4 +1,4 @@ -# https://github.com/Next-Flip/Momentum-Firmware/blob/dev/scripts/asset_packer.py +#!/usr/bin/env python from PIL import Image, ImageOps import heatshrink2 import pathlib From 6b6d98da2af9fda8051c631659264bd75fa49c8f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:21:42 +0300 Subject: [PATCH 15/26] merge js changes changes by Willy-JL spi module by jamisonderek --- applications/system/js_app/application.fam | 8 + applications/system/js_app/js_thread.c | 14 +- .../system/js_app/modules/js_gui/byte_input.c | 32 +- .../system/js_app/modules/js_gui/js_gui.c | 4 +- .../system/js_app/modules/js_gui/js_gui.h | 2 +- .../system/js_app/modules/js_gui/submenu.c | 4 +- .../system/js_app/modules/js_gui/text_input.c | 17 +- applications/system/js_app/modules/js_math.c | 2 +- applications/system/js_app/modules/js_spi.c | 283 ++++++++++++++++++ .../system/js_app/types/badusb/index.d.ts | 2 +- applications/system/js_app/types/global.d.ts | 6 +- .../system/js_app/types/math/index.d.ts | 3 + .../system/js_app/types/spi/index.d.ts | 30 ++ 13 files changed, 385 insertions(+), 22 deletions(-) create mode 100644 applications/system/js_app/modules/js_spi.c create mode 100644 applications/system/js_app/types/spi/index.d.ts diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index f8f1be13f4..5402bada78 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -193,3 +193,11 @@ App( requires=["js_app"], sources=["modules/js_i2c.c"], ) + +App( + appid="js_spi", + apptype=FlipperAppType.PLUGIN, + entry_point="js_spi_ep", + requires=["js_app"], + sources=["modules/js_spi.c"], +) diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 7a774d324b..83f9e604cd 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,12 +198,18 @@ static void js_require(struct mjs* mjs) { 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) == 1) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str)); - } else { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base)); + 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; diff --git a/applications/system/js_app/modules/js_gui/byte_input.c b/applications/system/js_app/modules/js_gui/byte_input.c index 5c8844d222..2d6dae4755 100644 --- a/applications/system/js_app/modules/js_gui/byte_input.c +++ b/applications/system/js_app/modules/js_gui/byte_input.c @@ -8,6 +8,7 @@ typedef struct { uint8_t* buffer; size_t buffer_size; + size_t default_data_size; FuriString* header; FuriSemaphore* input_semaphore; JsEventLoopContract contract; @@ -38,7 +39,14 @@ static bool len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) { UNUSED(mjs); UNUSED(input); - context->buffer_size = (size_t)(value.number); + 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, @@ -57,16 +65,24 @@ static bool default_data_assign( JsByteKbContext* context) { UNUSED(mjs); - mjs_val_t array_buf = value.array; + mjs_val_t array_buf = value.term; if(mjs_is_data_view(array_buf)) { array_buf = mjs_dataview_get_buf(mjs, array_buf); } - size_t default_data_len = 0; - char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &default_data_len); - memcpy( - context->buffer, - (uint8_t*)default_data, - MIN((size_t)context->buffer_size, default_data_len)); + 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, 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 4bd4ccc31b..22d04855d2 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -216,14 +216,14 @@ 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){.array = value}; + c_value = (JsViewPropValue){.term = value}; } break; case JsViewPropTypeBool: { if(!mjs_is_boolean(value)) { 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 67266b1fc3..d400d0a33c 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -16,8 +16,8 @@ typedef enum { 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 aecd413be4..c142bcddb6 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 d2bf4a8f93..e93bbfad0e 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -8,6 +8,7 @@ typedef struct { char* buffer; size_t buffer_size; + size_t default_text_size; FuriString* header; bool default_text_clear; FuriSemaphore* input_semaphore; @@ -49,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, @@ -70,6 +78,13 @@ static bool default_text_assign( 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); text_input_set_result_callback( input, diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index 7d54cf9b9f..cf66b6a44d 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_spi.c b/applications/system/js_app/modules/js_spi.c new file mode 100644 index 0000000000..c0f4d684dc --- /dev/null +++ b/applications/system/js_app/modules/js_spi.c @@ -0,0 +1,283 @@ +#include "../js_modules.h" +#include + +typedef struct { + bool acquired_bus; +} JsSpiInst; + +static JsSpiInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpiInst* spi = mjs_get_ptr(mjs, obj_inst); + furi_assert(spi); + return spi; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count_range(struct mjs* mjs, size_t min_count, size_t max_count) { + size_t num_args = mjs_nargs(mjs); + if(num_args < min_count || num_args > max_count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_spi_acquire(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 0, 0)) return; + JsSpiInst* spi = get_this_ctx(mjs); + if(!spi->acquired_bus) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + spi->acquired_bus = true; + } + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_spi_release(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 0, 0)) return; + JsSpiInst* spi = get_this_ctx(mjs); + if(spi->acquired_bus) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + spi->acquired_bus = false; + } + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool js_spi_is_acquired(struct mjs* mjs) { + JsSpiInst* spi = get_this_ctx(mjs); + return spi->acquired_bus; +} + +static void js_spi_write(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 0); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = furi_hal_spi_bus_tx(&furi_hal_spi_bus_handle_external, tx_buf, tx_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + if(tx_buf_was_allocated) free(tx_buf); + mjs_return(mjs, mjs_mk_boolean(mjs, result)); +} + +static void js_spi_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t rx_len_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + return; + } + + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = furi_hal_spi_bus_rx(&furi_hal_spi_bus_handle_external, rx_buf, rx_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + free(rx_buf); + mjs_return(mjs, ret); +} + +static void js_spi_write_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 0); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t data_len = 0; + if(mjs_is_array(tx_buf_arg)) { + data_len = mjs_array_length(mjs, tx_buf_arg); + if(data_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(data_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < data_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &data_len); + if(data_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint8_t* rx_buf = malloc(data_len); // RX and TX are same length for SPI writeRead. + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = + furi_hal_spi_bus_trx(&furi_hal_spi_bus_handle_external, tx_buf, rx_buf, data_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, data_len); + } + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + mjs_return(mjs, ret); +} + +static void* js_spi_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + JsSpiInst* spi = (JsSpiInst*)malloc(sizeof(JsSpiInst)); + spi->acquired_bus = false; + mjs_val_t spi_obj = mjs_mk_object(mjs); + mjs_set(mjs, spi_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, spi)); + mjs_set(mjs, spi_obj, "acquire", ~0, MJS_MK_FN(js_spi_acquire)); + mjs_set(mjs, spi_obj, "release", ~0, MJS_MK_FN(js_spi_release)); + mjs_set(mjs, spi_obj, "write", ~0, MJS_MK_FN(js_spi_write)); + mjs_set(mjs, spi_obj, "read", ~0, MJS_MK_FN(js_spi_read)); + mjs_set(mjs, spi_obj, "writeRead", ~0, MJS_MK_FN(js_spi_write_read)); + *object = spi_obj; + + furi_hal_spi_bus_handle_init(&furi_hal_spi_bus_handle_external); + return (void*)spi; +} + +static void js_spi_destroy(void* inst) { + JsSpiInst* spi = (JsSpiInst*)inst; + if(spi->acquired_bus) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + free(spi); + furi_hal_spi_bus_handle_deinit(&furi_hal_spi_bus_handle_external); +} + +static const JsModuleDescriptor js_spi_desc = { + "spi", + js_spi_create, + js_spi_destroy, + NULL, +}; + +static const FlipperAppPluginDescriptor spi_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_spi_desc, +}; + +const FlipperAppPluginDescriptor* js_spi_ep(void) { + return &spi_plugin_descriptor; +} diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/types/badusb/index.d.ts index 4fbda5ef8c..57c2662cdc 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/types/badusb/index.d.ts @@ -40,7 +40,7 @@ export type KeyCode = MainKey | ModifierKey | number; * * @param settings USB device settings. Omit to select default parameters */ -export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath: 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 diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index d132f89f58..a55dae7d96 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -31,8 +31,10 @@ declare const __filename: string; /** * @brief Reads a JS value from a file * - * Reads a file at the specified path, interprets it as a JS value and returns - * the last value pushed on the stack. + * Reads a file at the specified path and runs it as JS, returning the last evaluated value. + * + * The result is cached and this filepath will not re-evaluated on future + * load() calls for this session. * * @param path The path to the file * @param scope An object to use as global scope while running this file diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/types/math/index.d.ts index 25abca4af1..4924eea7e6 100644 --- a/applications/system/js_app/types/math/index.d.ts +++ b/applications/system/js_app/types/math/index.d.ts @@ -1,3 +1,4 @@ +export function isEqual(a: number, b: number, tolerance: number): boolean; export function abs(n: number): number; export function acos(n: number): number; export function acosh(n: number): number; @@ -12,6 +13,7 @@ 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 log(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; @@ -21,4 +23,5 @@ export function sin(n: number): number; export function sqrt(n: number): number; export function trunc(n: number): number; declare const PI: number; +declare const E: number; declare const EPSILON: number; diff --git a/applications/system/js_app/types/spi/index.d.ts b/applications/system/js_app/types/spi/index.d.ts new file mode 100644 index 0000000000..8d72bc29c8 --- /dev/null +++ b/applications/system/js_app/types/spi/index.d.ts @@ -0,0 +1,30 @@ +/** + * @brief Acquire SPI bus + */ +export declare function acquire(): void; + +/** + * @brief Release SPI bus + */ +export declare function release(): void; + +/** + * @brief Write data to SPI bus and return success status + * @param data The data to write + * @param timeout Timeout in milliseconds + */ +export declare function write(data: number[] | ArrayBuffer, timeout?: number): boolean; + +/** + * @brief Read data from SPI bus or return undefined on failure + * @param length How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function read(length: number, timeout?: number): ArrayBuffer | undefined; + +/** + * @brief Write and read data on SPI bus or return undefined on failure + * @param data The data to write, its length also indicates how many bytes will be read + * @param timeout Timeout in milliseconds + */ +export declare function writeRead(data: number[] | ArrayBuffer, timeout?: number): ArrayBuffer | undefined; From e630f44afde3f3fd57d68d1cf1421abe496aecb0 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:22:21 +0300 Subject: [PATCH 16/26] merge p2 --- .../examples/apps/Scripts/js_examples/gui.js | 8 +- .../apps/Scripts/js_examples/interactive.js | 5 +- .../examples/apps/Scripts/js_examples/spi.js | 88 +++++++++++++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 applications/system/js_app/examples/apps/Scripts/js_examples/spi.js diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js index 5faeb6d323..a1e023853e 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js @@ -17,18 +17,16 @@ let views = { empty: emptyView.make(), keyboard: textInputView.makeWith({ header: "Enter your name", - defaultText: flipper.getName(), - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 32, + defaultText: flipper.getName(), + defaultTextClear: true, }), helloDialog: dialogView.make(), bytekb: byteInputView.makeWith({ header: "Look ma, I'm a header text!", - defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), - // Props for makeWith() are passed in reverse order, so length must be after defaultData 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.", diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js index 34639cdaca..40ca98c309 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js @@ -24,11 +24,10 @@ let views = { }), textInput: textInput.makeWith({ header: "Type JavaScript Code:", - defaultText: "2+2", - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 256, + defaultText: "2+2", + defaultTextClear: true, }), loading: loading.make(), }; diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/spi.js b/applications/system/js_app/examples/apps/Scripts/js_examples/spi.js new file mode 100644 index 0000000000..810637b3bb --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/spi.js @@ -0,0 +1,88 @@ +// Connect a w25q32 SPI device to the Flipper Zero. +// D1=pin 2 (MOSI), SLK=pin 5 (SCK), GND=pin 8 (GND), D0=pin 3 (MISO), CS=pin 4 (CS), VCC=pin 9 (3V3) +let spi = require("spi"); + +// Display textbox so user can scroll to see all output. +let eventLoop = require("event_loop"); +let gui = require("gui"); +let text = "SPI demo\n"; +let textBox = require("gui/text_box").makeWith({ + focus: "end", + font: "text", + text: text, +}); + +function addText(add) { + text += add; + textBox.set("text", text); +} + +gui.viewDispatcher.switchTo(textBox); + +// writeRead returns a buffer the same length as the input buffer. +// We send 6 bytes of data, starting with 0x90, which is the command to read the manufacturer ID. +// Can also use Uint8Array([0x90, 0x00, ...]) as write parameter +// Optional timeout parameter in ms. We set to 100ms. +let data_buf = spi.writeRead([0x90, 0x0, 0x0, 0x0, 0x0, 0x0], 100); +let data = Uint8Array(data_buf); +if (data.length === 6) { + if (data[4] === 0xEF) { + addText("Found Winbond device\n"); + if (data[5] === 0x15) { + addText("Device ID: W25Q32\n"); + } else { + addText("Unknown device ID: " + data[5].toString(16) + "\n"); + } + } else if (data[4] === 0x0) { + addText("Be sure Winbond W25Q32 is connected to Flipper Zero SPI pins.\n"); + } else { + addText("Unknown device. Manufacturer ID: " + data[4].toString(16) + "\n"); + } +} + +addText("\nReading JEDEC ID\n"); + +// Acquire the SPI bus. Multiple calls will happen with Chip Select (CS) held low. +spi.acquire(); + +// Send command (0x9F) to read JEDEC ID. +// Can also use Uint8Array([0x9F]) as write parameter +// Note: you can pass an optional timeout parameter in milliseconds. +spi.write([0x9F]); + +// Request 3 bytes of data. +// Note: you can pass an optional timeout parameter in milliseconds. +data_buf = spi.read(3); + +// Release the SPI bus as soon as we are done with the set of SPI commands. +spi.release(); + +data = Uint8Array(data_buf); +addText("JEDEC MF ID: " + data[0].toString(16) + "\n"); +addText("JEDEC Memory Type: " + data[1].toString(16) + "\n"); +addText("JEDEC Capacity ID: " + data[2].toString(16) + "\n"); + +if (data[0] === 0xEF) { + addText("Found Winbond device\n"); +} +let capacity = data[1] << 8 | data[2]; +if (capacity === 0x4016) { + addText("Device: W25Q32\n"); +} else if (capacity === 0x4015) { + addText("Device: W25Q16\n"); +} else if (capacity === 0x4014) { + addText("Device: W25Q80\n"); +} else { + addText("Unknown device\n"); +} + +// Wait for user to close the app +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) { + eventLoop.stop(); +}, eventLoop); + +// This script has no interaction, only textbox, so event loop doesn't need to be running all the time +// We run it at the end to accept input for the back button press to quit +// But before that, user sees a textbox and pressing back has no effect +// This is fine because it allows simpler logic and the code above takes no time at all to run +eventLoop.run(); From 3d52187aa9b9c24bd7b6ee2a617deac5ead0493c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:45:02 +0300 Subject: [PATCH 17/26] ndef parser updates by luu176 and Willy-JL --- .../main/nfc/plugins/supported_cards/ndef.c | 1174 ++++++++++++----- 1 file changed, 866 insertions(+), 308 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index fe71915eb6..820324421d 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -1,41 +1,299 @@ // 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 +// 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++) { - const char c = buf[i]; - if((c < ' ' || c > '~') && c != '\r' && c != '\n') { - return false; + 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 - print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) { - if(prefix) furi_string_cat_printf(str, "%s: ", prefix); + 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(str, "%.*s", len, buf); + furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); } else { - for(uint8_t i = 0; i < len; i++) { - furi_string_cat_printf(str, "%02X ", buf[i]); + for(size_t i = 0; i < len; i++) { + furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); } } - furi_string_cat(str, "\n"); + 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; @@ -43,136 +301,164 @@ static inline uint8_t hex_to_int(char c) { return 0; } -static char decode_char(const char* str) { - return (hex_to_int(str[1]) << 4) | hex_to_int(str[2]); +static char url_decode_char(const char* str) { + return (hex_to_int(str[0]) << 4) | hex_to_int(str[1]); } -static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 - const char* prepends[] = { - [0x00] = "", - [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:", - }; - const char* prepend = ""; - uint8_t prepend_type = payload[0]; - if(prepend_type < COUNT_OF(prepends)) { - prepend = prepends[prepend_type]; +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]; } - size_t prepend_len = strlen(prepend); - - size_t uri_len = prepend_len + (payload_len - 1); - char* const uri_buf = malloc(uri_len); // const to keep the original pointer to free later - memcpy(uri_buf, prepend, prepend_len); - memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1); - char* uri = uri_buf; // cursor we can iterate and shift freely - - // Encoded chars take 3 bytes (%AB), decoded chars take 1 byte - // We can decode by iterating and overwriting the same buffer - size_t decoded_len = 0; - for(size_t encoded_idx = 0; encoded_idx < uri_len; encoded_idx++) { - if(uri[encoded_idx] == '%' && encoded_idx + 2 < uri_len) { - char hi = toupper(uri[encoded_idx + 1]); - char lo = toupper(uri[encoded_idx + 2]); - if(((hi >= 'A' && hi <= 'F') || (hi >= '0' && hi <= '9')) && - ((lo >= 'A' && lo <= 'F') || (lo >= '0' && lo <= '9'))) { - uri[decoded_len++] = decode_char(&uri[encoded_idx]); - encoded_idx += 2; - continue; - } + 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 } - uri[decoded_len++] = uri[encoded_idx]; } - const char* type = "URI"; - if(strncmp(uri, "http", 4) == 0) { - type = "URL"; - } else if(strncmp(uri, "tel:", 4) == 0) { - type = "Phone"; - uri += 4; - decoded_len -= 4; - } else if(strncmp(uri, "mailto:", 7) == 0) { - type = "Mail"; - uri += 7; - decoded_len -= 7; + // 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; + } } - furi_string_cat_printf(str, "%s\n", type); - print_data(str, NULL, (uint8_t*)uri, decoded_len, false); + // Print static data as-is + furi_string_cat_printf(ndef->output, "%s\n", type); + if(prepend) { + furi_string_cat(ndef->output, prepend); + } - free(uri_buf); + // 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 void parse_ndef_text(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - furi_string_cat(str, "Text\n"); - print_data(str, NULL, payload + 3, payload_len - 3, false); +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 void parse_ndef_bt(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - furi_string_cat(str, "BT MAC\n"); - print_data(str, NULL, payload + 2, payload_len - 2, 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 void parse_ndef_vcard(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - char* tmp = malloc(payload_len + 1); - memcpy(tmp, payload, payload_len); - tmp[payload_len] = '\0'; - FuriString* fmt = furi_string_alloc_set(tmp); - free(tmp); - - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "BEGIN:VCARD")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - if(furi_string_end_with(fmt, "END:VCARD")) { - furi_string_left(fmt, furi_string_search_rchar(fmt, '\n')); +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; } - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "VERSION:")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - furi_string_trim(fmt); + } + + // 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; } } - furi_string_cat(str, "Contact\n"); - print_data(str, NULL, (uint8_t*)furi_string_get_cstr(fmt), furi_string_size(fmt), false); - furi_string_free(fmt); + // 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; } -static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t payload_len) { -// https://android.googlesource.com/platform/packages/apps/Nfc/+/refs/heads/main/src/com/android/nfc/NfcWifiProtectedSetup.java +// 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) @@ -186,44 +472,57 @@ static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t pa #define AUTH_TYPE_WPA_AND_WPA2_PSK (0x0022) #define MAX_NETWORK_KEY_SIZE_BYTES (64) - size_t i = 0; - while(i < payload_len) { - uint16_t field_id = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - uint16_t field_len = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + 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) { - furi_string_cat(str, "WiFi\n"); - size_t start_position = i; - while(i < start_position + field_len) { - uint16_t cfg_id = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - uint16_t cfg_len = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - - if(i + cfg_len > start_position + field_len) { - return; + 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: - print_data(str, "SSID", payload + i, cfg_len, false); - i += cfg_len; + 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; + return false; } - print_data(str, "PWD", payload + i, cfg_len, false); - i += cfg_len; + 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; + return false; } - short auth_type = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + 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: @@ -248,250 +547,507 @@ static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t pa auth = "Unknown"; break; } - print_data(str, "AUTH", (uint8_t*)auth, strlen(auth), false); + ndef_print(ndef, "AUTH", auth, strlen(auth), false); break; default: - i += cfg_len; + pos += cfg_len; break; } } - return; + return true; } - i += field_len; + pos += field_len; } + + furi_string_cat(ndef->output, "No data parsed\n"); + return true; } -static void parse_ndef_payload( - FuriString* str, - uint8_t tnf, +// ---=== 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, - const uint8_t* payload, - uint32_t payload_len) { - if(!payload_len) { - furi_string_cat(str, "Empty\n"); - return; + uint8_t type_len) { + FURI_LOG_D(TAG, "payload type: %.*s len: %d", type_len, type, len); + if(!len) { + furi_string_cat(ndef->output, "Empty\n"); + return true; } + switch(tnf) { - case 0x01: // NFC Forum well-known type [NFC RTD] - if(strncmp("U", type, type_len) == 0) { - parse_ndef_uri(str, payload, payload_len); + 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) { - parse_ndef_text(str, payload, payload_len); - } else { - print_data(str, "Well-known type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); + return ndef_parse_text(ndef, pos, len); } - break; - case 0x02: // Media-type [RFC 2046] + // 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) { - parse_ndef_bt(str, payload, payload_len); + return ndef_parse_bt(ndef, pos, len); } else if(strncmp("text/vcard", type, type_len) == 0) { - parse_ndef_vcard(str, payload, payload_len); + return ndef_parse_vcard(ndef, pos, len); } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { - parse_ndef_wifi(str, payload, payload_len); - } else { - print_data(str, "Media Type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); + return ndef_parse_wifi(ndef, pos, len); } - break; - case 0x00: // Empty - case 0x03: // Absolute URI [RFC 3986] - case 0x04: // NFC Forum external type [NFC RTD] - case 0x05: // Unknown - case 0x06: // Unchanged - case 0x07: // Reserved - default: // Unknown // Dump data without parsing - print_data(str, "Type name format", &tnf, 1, true); - print_data(str, "Type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); - break; + 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; } } -static const uint8_t* parse_ndef_message( - FuriString* str, - size_t message_num, - const uint8_t* cur, - const uint8_t* message_end) { - // NDEF message and record documentation: - // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/protocols/nfc/index.html#ndef-message-and-record-format +// 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(cur < message_end) { + while(pos < end) { // Flags and TNF - uint8_t flags_tnf = *cur++; + 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 & (1 << 7)) break; + if(record_num++ && flags_tnf.message_begin) return false; // Message End should only be set on last record - if(last_record) break; - if(flags_tnf & (1 << 6)) last_record = true; - // Chunked Flag not supported - if(flags_tnf & (1 << 5)) break; - // Payload Length field of 1 vs 4 bytes - bool short_record = flags_tnf & (1 << 4); - // Is payload ID length and value present - bool id_present = flags_tnf & (1 << 3); - // Type Name Format 3 bit value - uint8_t tnf = flags_tnf & 0b00000111; + 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 = *cur++; + uint8_t type_len; + if(!ndef_get(ndef, pos++, 1, &type_len)) return false; - // Payload Length + // Payload Length field of 1 or 4 bytes uint32_t payload_len; - if(short_record) { - payload_len = *cur++; + 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 { - payload_len = bit_lib_bytes_to_num_be(cur, 4); - cur += 4; + 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(id_present) { - id_len = *cur++; + if(flags_tnf.id_length_present) { + if(!ndef_get(ndef, pos++, 1, &id_len)) return false; } // Payload Type - char* type = NULL; + 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) { - type = malloc(type_len); - memcpy(type, cur, type_len); - cur += 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 - cur += id_len; + pos += id_len; - furi_string_cat_printf(str, "\e*> M:%d R:%d - ", message_num, record_num); - parse_ndef_payload(str, tnf, type, type_len, cur, payload_len); - cur += payload_len; + if(smart_poster) { + furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", 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; - free(type); - furi_string_trim(str, "\n"); - furi_string_cat(str, "\n\n"); + if(type_was_allocated) free(type); + furi_string_trim(ndef->output, "\n"); + furi_string_cat(ndef->output, "\n\n"); } - return cur; + + 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%d: Empty\n\n", message_num); + } + } + + return pos == end && (last_record || record_num == 0); } -static bool ndef_parse(const NfcDevice* device, FuriString* parsed_data) { +// 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); - bool parsed = false; + // 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; + } - do { - // Memory layout documentation: - // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrfxlib/nfc/doc/type_2_tag.html#id2 + // 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; - // 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) { - break; - } + // 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; - // Double check Capability Container (CC) and find data area bounds - struct { - uint8_t nfc_magic_number; - uint8_t document_version_number; - uint8_t data_area_size; - uint8_t read_write_access; - }* cc = (void*)&data->page[3].data[0]; - if(cc->nfc_magic_number != 0xE1) break; - if(cc->document_version_number != 0x10) break; - const uint8_t* cur = &data->page[4].data[0]; - const uint8_t* end = cur + (cc->data_area_size * 2 * MF_ULTRALIGHT_PAGE_SIZE); - 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 message_num = 0; - - // Parse as TLV (see docs above) - while(cur < end) { - switch(*cur++) { - case 0x03: { // NDEF message - if(cur >= end) break; - uint16_t len; - if(*cur < 0xFF) { // 1 byte length - len = *cur++; - } else { // 3 byte length (0xFF marker + 2 byte integer) - if(cur + 2 >= end) { - cur = end; - break; - } - len = bit_lib_bytes_to_num_be(++cur, 2); - cur += 2; - } - if(cur + len >= end) { - cur = end; - break; - } + NDEF_TITLE(device, parsed_data); - if(message_num++ == 0) { - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); - } + 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; +} - const uint8_t* message_end = cur + len; - cur = parse_ndef_message(parsed_data, message_num, cur, message_end); - if(cur != message_end) cur = end; +#elif NDEF_PROTO == NDEF_PROTO_MFC - break; +// 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; } + } + } - case 0xFE: // TLV end - cur = end; - if(message_num != 0) parsed = true; - break; + NDEF_TITLE(device, parsed_data); - case 0x00: // Padding, has no length, skip - break; + // 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; - case 0x01: // Lock control - case 0x02: // Memory control - case 0xFD: // Proprietary - // We don't care, skip this TLV block - if(cur >= end) break; - if(*cur < 0xFF) { // 1 byte length - cur += *cur + 1; // Shift by TLV length - } else { // 3 byte length (0xFF marker + 2 byte integer) - if(cur + 2 >= end) { - cur = end; - break; - } - cur += bit_lib_bytes_to_num_be(cur + 1, 2) + 3; // Shift by TLV length - } - break; + Ndef ndef = { + .output = parsed_data, + .mfc = + { + .blocks = data->block, + .size = data_size, + }, + }; + size_t total_parsed = 0; - default: // Unknown, bail to avoid problems - cur = end; - break; + 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: %d", 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_reset(parsed_data); + furi_string_left(parsed_data, string_prev); } - } while(false); + } - return parsed; + 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: 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 = { - .protocol = NfcProtocolMfUltralight, .verify = NULL, .read = NULL, - .parse = ndef_parse, +#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 */ @@ -505,3 +1061,5 @@ static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { const FlipperAppPluginDescriptor* ndef_plugin_ep(void) { return &ndef_plugin_descriptor; } + +#endif From e7b64c843c2408c3c8aac780ecb12c4727dfb16e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:50:07 +0300 Subject: [PATCH 18/26] upd manifest --- applications/main/nfc/application.fam | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index a78a359503..3eb24096b1 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -246,8 +246,28 @@ App( ) App( - appid="ndef_parser", + 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"], From ed57ddb14be48be3134bbca57726cff247e1d6e1 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:28:33 +0300 Subject: [PATCH 19/26] NFC: Plaintain parser fix by mxcdoam https://github.com/flipperdevices/flipperzero-firmware/pull/3975/files --- .../nfc/plugins/supported_cards/plantain.c | 84 +++++++++++++++++-- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index c38140de27..9f2491691b 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); From bb922de569a2c09159a1365f2d1e875a9895d669 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:36:44 +0300 Subject: [PATCH 20/26] upd changelog --- CHANGELOG.md | 67 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4cd00145..4665f45a60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,43 +1,45 @@ ## Main changes - SubGHz: - Frequency analyzer fixes and improvements: - - Enforce int module (like in OFW) usage due to lack of required hardware on external boards (PathIsolate (+rf switch for multiple paths)) and incorrect usage and/or understanding the purpose of frequency analyzer app by users, it should be used only to get frequency of the remote placed around 1-10cm around flipper's left corner - - Fix possible GSM mobile towers signal interference by limiting upper frequency to 920mhz max + - **Enforce int module** (like in OFW) usage due to lack of required hardware on external boards (PathIsolate (+rf switch for multiple paths)) and incorrect usage and/or understanding the purpose of frequency analyzer app by users, it should be used only to get frequency of the remote placed around 1-10cm around flipper's left corner + - **Fix possible GSM mobile towers signal interference** by limiting upper frequency to 920mhz max - Fix duplicated frequency lists and use user config for nearest frequency selector too - - Fix buttons logic, fix crash + - Fix buttons logic, **fix crash** - Protocol improvements: - - Keeloq: Monarch full support, with add manually option (thanks to anonymous contributor (TBA)) - - Princeton support for second button encoding type (8bit) + - **Keeloq: Monarch full support, with add manually option** (thanks to anonymous contributor (TBA)) + - **Princeton support for second button encoding type** (8bit) - GangQi fix serial check and remove broken check from UI - Hollarm add more button codes (thanks to @mishamyte for captures) - Misc: - Add extra settings to disable GPIO pins control used for external modules amplifiers and/or LEDs (in radio settings menu with debug ON) - NFC: - - Read Ultralight block by block (by @mishamyte | PR #825 #826) - - Update NDEF parser (by @jaylikesbunda and @Willy-JL) - - OFW PR 3822: MIFARE Classic Key Recovery Improvements (by @noproto) + - Read Ultralight block by block (**fix password protected MFUL reading issue**) (by @mishamyte | PR #825 #826) + - **Update NDEF parser** (SLIX and MFC support) (by @luu176 and @jaylikesbunda and @Willy-JL) + - OFW PR 3822: **MIFARE Classic Key Recovery Improvements** (by @noproto) - OFW PR 3930: NFC Emulation freeze (by @RebornedBrain) - OFW PR 3885: Add API to enforce ISO15693 mode (by @aaronjamt) - OFW: iso14443_4a improvements (by @RebornedBrain) - - OFW: Plantain parser improvements (by @assasinfil) + - OFW: Plantain parser improvements (by @assasinfil) & fixes (by @mxcdoam) - OFW: Moscow social card parser (by @assasinfil) - OFW: NFC: H World Hotel Chain Room Key Parser - OFW: NFC Parser for Tianjin Railway Transit - OFW: NFC TRT Parser: Additional checks to prevent false positives - New keys in system dict - Infrared: - - Add LEDs universal remote (DB by @amec0e) + - **Add LEDs universal remote** (DB by @amec0e) - Update universal remote assets (by @amec0e | PR #813 #816) - JS: - OFW: JS modules -> **Breaking API change** - - Backporting custom features - WIP (by @xMasterX and @Willy-JL) + - **Backporting custom features** (read about most of the changes after other changes section) (by @xMasterX and @Willy-JL) - Add i2c module (by @jamisonderek) + - Add SPI module (by @jamisonderek) * OFW: FuriHal, drivers: rework gauge initialization routine -> **Downgrade to older releases will break battery UI percent indicator, upgrade to this or newer version to restore** * OFW: heap: increased size -> **More free RAM!!** * OFW: New layout for BadUSB (es-LA) * OFW: Require PIN on boot * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* OFW PR 3971: Fix JS memory corruption (in gpio module) (by @portasynthinca3) * OFW: lib: digital_signal: digital_sequence: add furi_hal.h wrapped in ifdefs * OFW: Add warning about stealth mode in vibro CLI * OFW: Small fixes in the wifi devboard docs @@ -59,6 +61,49 @@ * OFW: Folder rename fails * OFW: Put errno into TCB * OFW: Fix USB-UART bridge exit screen stopping the bridge prematurely +**More details on JS changes** (js changelog written by @Willy-JL , thanks!): +- Non-exhaustive list of changes to help you fix your scripts: + - `badusb`: + - `setup()`: `mfr_name`, `prod_name`, `layout_path` parameters renamed to `mfrName`, `prodName`, `layoutPath` + - effort required to update old scripts using badusb: very minimal + - `dialog`: + - removed, now replaced by `gui/dialog` and `gui/file_picker` (see below) + - `event_loop`: + - new module, allows timer functionality, callbacks and event-driven programming, used heavily alongside gpio and gui modules + - `gpio`: + - fully overhauled, now you `get()` pin instances and perform actions on them like `.init()` + - now supports interrupts, callbacks and more cool things + - effort required to update old scripts using gpio: moderate + - `gui`: + - new module, fully overhauled, replaces dialog, keyboard, submenu, textbox modules + - higher barrier to entry than older modules (requires usage of `event_loop` and `gui.viewDispatcher`), but much more flexible, powerful and easier to extend + - includes all previously available js gui functionality (except `widget`), and also adds `gui/loading` and `gui/empty_screen` views + - currently `gui/file_picker` works different than other new view objects, it is a simple `.pickFile()` synchronous function, but this [may change later](https://github.com/flipperdevices/flipperzero-firmware/pull/3961#discussion_r1805579153) + - effort required to update old scripts using gui: extensive + - `keyboard`: + - removed, now replaced by `gui/text_input` and `gui/byte_input` (see above) + - `math`: + - `is_equal()` renamed to `isEqual()` + - `storage`: + - fully overhauled, now you `openFile()`s and perform actions on them like `.read()` + - now supports many more operations including different open modes, directories and much more + - effort required to update old scripts using storage: moderate + - `submenu`: + - removed, now replaced by `gui/submenu` (see above) + - `textbox`: + - removed, now replace by `gui/text_box` (see above) + - `widget`: + - only gui functionality not ported to new gui module, remains unchanged for now but likely to be ported later on + - globals: + - `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs + - `to_string()` renamed and moved to number class as `n.toString()`, now supports optional base parameter + - `to_hex_string()` removed, now use `n.toString(16)` + - `parse_int()` renamed to `parseInt()`, now supports optional base parameter + - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `s.toUpperCase()` and `s.toLowerCase()` + - effort required to update old scripts using these: minimal + - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript) + - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct + - Type definitions for extra modules we have that OFW doesn't will come later

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From b462329dd52c191446075ad4b4426df98a1f9f36 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:51:49 +0300 Subject: [PATCH 21/26] upd changelog [ci skip] --- CHANGELOG.md | 2 +- ReadMe.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4665f45a60..ff636a393e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Fix duplicated frequency lists and use user config for nearest frequency selector too - Fix buttons logic, **fix crash** - Protocol improvements: - - **Keeloq: Monarch full support, with add manually option** (thanks to anonymous contributor (TBA)) + - **Keeloq: Monarch full support, with add manually option** (thanks @ashphx !) - **Princeton support for second button encoding type** (8bit) - GangQi fix serial check and remove broken check from UI - Hollarm add more button codes (thanks to @mishamyte for captures) diff --git a/ReadMe.md b/ReadMe.md index ac4c442f8d..61fe9ca7b5 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -117,7 +117,7 @@ Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX: - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) -- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks anonymous contributor (TBA)) +- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks @ashphx !) Protocols support made by Skorp (original implementation) and @xMasterX (current version): - CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) From 73241a6e70a1678bc4391fbb022a525666080a56 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:07:19 +0000 Subject: [PATCH 22/26] Sync apps No actual app updates Mr Christopher smartass Fails if you see this, you can shut up thank you There's been no app updates in 4 days Last time I pulled was 25th Oct I pulled now on 29th Oct and there were no updates So you have no place saying "Uh oh ... Looks like rogue Masters official release has more up-to-date apps than momentums dev build currently.." If anything, RM updated later than I did --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index 067aa52f52..da86546e7c 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 067aa52f521e3f05e2181f753bffab733c424602 +Subproject commit da86546e7cc0351f5f6a7cdf6e8c0ab63953d6eb From 38471fdd75311c2cd1f6bb5c948dfa841c305b4d Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:08:52 +0000 Subject: [PATCH 23/26] GUI: Refactor TextInput illegal symbols API --- CHANGELOG.md | 2 + applications/external | 2 +- .../services/gui/modules/text_input.c | 144 +++++++----------- .../services/gui/modules/text_input.h | 11 +- .../system/js_app/modules/js_gui/text_input.c | 2 +- targets/f7/api_symbols.csv | 3 +- 6 files changed, 68 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a50ef4684..a8c8a5286b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript) - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct - Type definitions for extra modules we have that OFW doesn't will come later +- GUI: Refactored TextInput illegal symbols (by @Willy-JL) + - If your app used `text_input_add_illegal_symbols(text_input)` it should change to `text_input_show_illegal_symbols(text_input, true)` ### Added: - Apps: diff --git a/applications/external b/applications/external index da86546e7c..d0300c7a5a 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit da86546e7cc0351f5f6a7cdf6e8c0ab63953d6eb +Subproject commit d0300c7a5a353ba0c73030c81afc75ec440797e6 diff --git a/applications/services/gui/modules/text_input.c b/applications/services/gui/modules/text_input.c index 58c53653ad..b4a9ce46e6 100644 --- a/applications/services/gui/modules/text_input.c +++ b/applications/services/gui/modules/text_input.c @@ -2,7 +2,6 @@ #include #include #include -#include struct TextInput { View* view; @@ -38,10 +37,10 @@ typedef struct { FuriString* validator_text; bool validator_message_visible; - char extra_symbols[9]; + bool illegal_symbols; bool cursor_select; - size_t cursor_pos; uint8_t selected_keyboard; + size_t cursor_pos; } TextInputModel; static const uint8_t keyboard_origin_x = 1; @@ -102,7 +101,7 @@ static const TextInputKey keyboard_keys_row_3[] = { {'9', 120, 32}, }; -static TextInputKey symbol_keyboard_keys_row_1[] = { +static const TextInputKey symbol_keyboard_keys_row_1[] = { {'!', 2, 8}, {'@', 12, 8}, {'#', 22, 8}, @@ -118,7 +117,7 @@ static TextInputKey symbol_keyboard_keys_row_1[] = { {'3', 120, 8}, }; -static TextInputKey symbol_keyboard_keys_row_2[] = { +static const TextInputKey symbol_keyboard_keys_row_2[] = { {'~', 2, 20}, {'+', 12, 20}, {'-', 22, 20}, @@ -133,7 +132,7 @@ static TextInputKey symbol_keyboard_keys_row_2[] = { {'6', 120, 20}, }; -static TextInputKey symbol_keyboard_keys_row_3[] = { +static const TextInputKey symbol_keyboard_keys_row_3[] = { {SWITCH_KEYBOARD_KEY, 0, 23}, {'.', 15, 32}, {',', 29, 32}, @@ -241,6 +240,33 @@ static char char_to_uppercase(const char letter) { } } +static char char_to_illegal_symbol(char original) { + switch(original) { + default: + return original; + case '0': + return '_'; + case '1': + return '<'; + case '2': + return '>'; + case '3': + return ':'; + case '4': + return '"'; + case '5': + return '/'; + case '6': + return '\\'; + case '7': + return '|'; + case '8': + return '?'; + case '9': + return '*'; + } +} + static void text_input_backspace_cb(TextInputModel* model) { if(model->clear_default_text) { model->text_buffer[0] = 0; @@ -356,7 +382,8 @@ static void text_input_view_draw_callback(Canvas* canvas, void* _model) { keyboard_origin_x + keys[column].x, keyboard_origin_y + keys[column].y - (glyph == '_' || char_is_lowercase(glyph)), - glyph); + (symbols && model->illegal_symbols) ? char_to_illegal_symbol(glyph) : + glyph); } } } @@ -479,6 +506,10 @@ static void text_input_handle_ok(TextInput* text_input, TextInputModel* model, I model->selected_keyboard != symbol_keyboard.keyboard_index) { selected = char_to_uppercase(selected); } + if(model->selected_keyboard == symbol_keyboard.keyboard_index && + model->illegal_symbols) { + selected = char_to_illegal_symbol(selected); + } if(model->clear_default_text) { model->text_buffer[0] = selected; model->text_buffer[1] = '\0'; @@ -622,35 +653,34 @@ static bool text_input_view_ascii_callback(AsciiEvent* event, void* context) { true); return true; default: // Look in keyboards + TextInputModel* model = view_get_model(text_input->view); + uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0; + bool uppercase = model->clear_default_text || text_length == 0; for(size_t k = 0; k < keyboard_count; k++) { + bool symbols = k == symbol_keyboard.keyboard_index; const Keyboard* keyboard = keyboards[k]; for(size_t r = 0; r < keyboard_row_count; r++) { const TextInputKey* row = get_row(keyboard, r); uint8_t size = get_row_size(keyboard, r); for(size_t key = 0; key < size; key++) { char lower = row[key].text; - char upper = char_to_uppercase(lower); + if(symbols && model->illegal_symbols) lower = char_to_illegal_symbol(lower); + char upper = symbols ? lower : char_to_uppercase(lower); if(event->value == lower || event->value == upper) { - with_view_model( - text_input->view, - TextInputModel * model, - { - model->cursor_select = false; - model->selected_keyboard = k; - model->selected_row = r; - model->selected_column = key; - bool shift = - (event->value == upper) != - (model->clear_default_text || strlen(model->text_buffer) == 0); - text_input_handle_ok( - text_input, model, shift ? InputTypeLong : InputTypeShort); - }, - true); + model->cursor_select = false; + model->selected_keyboard = k; + model->selected_row = r; + model->selected_column = key; + bool shift = (event->value == upper) != uppercase && !symbols; + text_input_handle_ok( + text_input, model, shift ? InputTypeLong : InputTypeShort); + view_commit_model(text_input->view, true); return true; } } } } + view_commit_model(text_input->view, false); break; } @@ -668,33 +698,6 @@ void text_input_timer_callback(void* context) { true); } -static void reset_extra_symbols(TextInputModel* model) { - memset(model->extra_symbols, 0, sizeof(model->extra_symbols)); - size_t symbol = 0; - for(size_t row = 0; row < 3; row++) { - size_t size = get_row_size(&symbol_keyboard, row) - 3; - for(size_t i = 0; i < 3; i++) { - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, row)[size++].text, '1' + (symbol++)); - } - } - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, 0)[get_row_size(&symbol_keyboard, 0) - 4].text, '0'); -} - -static void apply_extra_symbols(TextInputModel* model) { - size_t symbol = 0; - for(size_t row = 0; row < 3; row++) { - size_t size = get_row_size(&symbol_keyboard, row) - 3; - for(size_t i = 0; i < 3; i++) { - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, row)[size++].text, model->extra_symbols[symbol++]); - } - } - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, 0)[get_row_size(&symbol_keyboard, 0) - 4].text, '_'); -} - TextInput* text_input_alloc(void) { TextInput* text_input = malloc(sizeof(TextInput)); text_input->view = view_alloc(); @@ -712,7 +715,7 @@ TextInput* text_input_alloc(void) { { model->validator_text = furi_string_alloc(); model->minimum_length = 1; - reset_extra_symbols(model); + model->illegal_symbols = false; model->cursor_pos = 0; model->cursor_select = false; }, @@ -752,7 +755,7 @@ void text_input_reset(TextInput* text_input) { model->selected_column = 0; model->selected_keyboard = 0; model->minimum_length = 1; - reset_extra_symbols(model); + model->illegal_symbols = false; model->clear_default_text = false; model->cursor_pos = 0; model->cursor_select = false; @@ -813,43 +816,10 @@ void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length) true); } -void text_input_add_extra_symbol(TextInput* text_input, char symbol) { - furi_check(text_input); - if(!symbol) return; - with_view_model( - text_input->view, - TextInputModel * model, - { - for(size_t i = 0; i < sizeof(model->extra_symbols); i++) { - if(!model->extra_symbols[i]) { - model->extra_symbols[i] = symbol; - apply_extra_symbols(model); - break; - } - } - }, - true); -} - -void text_input_add_illegal_symbols(TextInput* text_input) { +void text_input_show_illegal_symbols(TextInput* text_input, bool show) { furi_check(text_input); with_view_model( - text_input->view, - TextInputModel * model, - { - size_t i = 0; - model->extra_symbols[i++] = '<'; - model->extra_symbols[i++] = '>'; - model->extra_symbols[i++] = ':'; - model->extra_symbols[i++] = '"'; - model->extra_symbols[i++] = '/'; - model->extra_symbols[i++] = '\\'; - model->extra_symbols[i++] = '|'; - model->extra_symbols[i++] = '?'; - model->extra_symbols[i++] = '*'; - apply_extra_symbols(model); - }, - true); + text_input->view, TextInputModel * model, { model->illegal_symbols = show; }, true); } void text_input_set_validator( diff --git a/applications/services/gui/modules/text_input.h b/applications/services/gui/modules/text_input.h index 7656a21bf4..6353d97327 100644 --- a/applications/services/gui/modules/text_input.h +++ b/applications/services/gui/modules/text_input.h @@ -77,11 +77,12 @@ void text_input_set_validator( TextInputValidatorCallback callback, void* callback_context); -// Add up to 9 extra characters for symbol keyboard -void text_input_add_extra_symbol(TextInput* text_input, char symbol); - -// Add the 9 predefined illegal (windows) symbols <>:"/\|?* -void text_input_add_illegal_symbols(TextInput* text_input); +/** + * @brief Show the 9 illegal (windows) symbols <>:"/\|?* in the symbols keyboard instead of the numbers + * @param [in] text_input TextInput + * @param [in] show Whether to show the illegal symbols or not + */ +void text_input_show_illegal_symbols(TextInput* text_input, bool show); TextInputValidatorCallback text_input_get_validator_callback(TextInput* text_input); 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 79a1383884..38dabb488b 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -135,7 +135,7 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_ .transformer_context = context, }, }; - text_input_add_illegal_symbols(input); + text_input_show_illegal_symbols(input, true); // Temporary until prop is available text_input_set_result_callback( input, (TextInputCallback)input_callback, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 70011042f8..55c66b02d7 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3669,8 +3669,6 @@ Function,+,text_box_reset,void,TextBox* Function,+,text_box_set_focus,void,"TextBox*, TextBoxFocus" Function,+,text_box_set_font,void,"TextBox*, TextBoxFont" Function,+,text_box_set_text,void,"TextBox*, const char*" -Function,+,text_input_add_extra_symbol,void,"TextInput*, char" -Function,+,text_input_add_illegal_symbols,void,TextInput* Function,+,text_input_alloc,TextInput*, Function,+,text_input_free,void,TextInput* Function,+,text_input_get_validator_callback,TextInputValidatorCallback,TextInput* @@ -3681,6 +3679,7 @@ Function,+,text_input_set_header_text,void,"TextInput*, const char*" Function,+,text_input_set_minimum_length,void,"TextInput*, size_t" Function,+,text_input_set_result_callback,void,"TextInput*, TextInputCallback, void*, char*, size_t, _Bool" Function,+,text_input_set_validator,void,"TextInput*, TextInputValidatorCallback, void*" +Function,+,text_input_show_illegal_symbols,void,"TextInput*, _Bool" Function,-,tgamma,double,double Function,-,tgammaf,float,float Function,-,tgammal,long double,long double From 4ee5789168d581a1b5f7cb1555e8dc087a13b907 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:19:15 +0000 Subject: [PATCH 24/26] GUI: Ascii input for ByteInput --- CHANGELOG.md | 1 + .../services/gui/modules/byte_input.c | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c8a5286b..88f7a3a688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ - OFW: Dolphin: Happy mode in Desktop settings (by @portasynthinca3) - OFW: CLI: Improvements part I, `neofetch` command (by @portasynthinca3), fix for lab.flipper.net (by @xMasterX) - GUI: + - ByteInput supports ASCII input (by @Willy-JL) - OFW: Add up and down button drawing functions to GUI elements (by @DerSkythe) - OFW: Extended icon draw function in Canvas (by @RebornedBrain) - OFW: RPC: Support 5V on GPIO control for ext. modules (by @gsurkov) diff --git a/applications/services/gui/modules/byte_input.c b/applications/services/gui/modules/byte_input.c index 3e23ee6c14..c9f6896625 100644 --- a/applications/services/gui/modules/byte_input.c +++ b/applications/services/gui/modules/byte_input.c @@ -726,6 +726,51 @@ static bool byte_input_view_input_callback(InputEvent* event, void* context) { return consumed; } +static bool byte_input_view_ascii_callback(AsciiEvent* event, void* context) { + ByteInput* byte_input = context; + furi_assert(byte_input); + + switch(event->value) { + case AsciiValueDC3: // Right + case AsciiValueDC4: // Left + with_view_model( + byte_input->view, + ByteInputModel * model, + { + if(event->value == AsciiValueDC3) { + byte_input_inc_selected_byte_mini(model); + } else { + byte_input_dec_selected_byte_mini(model); + } + }, + true); + return true; + default: // Look in keyboard + for(size_t r = 0; r < keyboard_row_count; r++) { + const ByteInputKey* row = byte_input_get_row(r); + uint8_t size = byte_input_get_row_size(r); + for(size_t key = 0; key < size; key++) { + char value = row[key].value; + if(event->value == value) { + with_view_model( + byte_input->view, + ByteInputModel * model, + { + model->selected_row = r; + model->selected_column = key; + byte_input_handle_ok(model); + }, + true); + return true; + } + } + } + break; + } + + return false; +} + /** Reset all input-related data in model * * @param model The model @@ -747,6 +792,7 @@ ByteInput* byte_input_alloc(void) { view_allocate_model(byte_input->view, ViewModelTypeLocking, sizeof(ByteInputModel)); view_set_draw_callback(byte_input->view, byte_input_view_draw_callback); view_set_input_callback(byte_input->view, byte_input_view_input_callback); + view_set_ascii_callback(byte_input->view, byte_input_view_ascii_callback); with_view_model( byte_input->view, From 45f6e448c6b7e5bbb43963049b40943b50f7e66f Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:52:58 +0000 Subject: [PATCH 25/26] RPC: Fix apps not updating and staying at 100% --nobuild --- CHANGELOG.md | 1 + applications/services/rpc/rpc_storage.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f7a3a688..f47d67abbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,6 +177,7 @@ - Fallback SD format prompt when storage settings is unavailable (by @Willy-JL) - OFW: Fix folder rename fails (by @portasynthinca3) - About: Fix BLE stack version string (by @Willy-JL) +- RPC: Fixed apps not updating and staying at 100% (by @Willy-JL) - OFW: Loader: Warn about missing SD card for main apps (by @Willy-JL) - NFC: - UL: Read Ultralight block by block (by @mishamyte) diff --git a/applications/services/rpc/rpc_storage.c b/applications/services/rpc/rpc_storage.c index 79b91d947f..10261461c6 100644 --- a/applications/services/rpc/rpc_storage.c +++ b/applications/services/rpc/rpc_storage.c @@ -629,7 +629,7 @@ static void rpc_system_storage_rename_process(const PB_Main* request, void* cont rpc_system_storage_reset_state(rpc_storage, session, true); if(path_contains_only_ascii(request->content.storage_rename_request.new_path)) { - FS_Error error = storage_common_rename_safe( + FS_Error error = storage_common_rename( rpc_storage->api, request->content.storage_rename_request.old_path, request->content.storage_rename_request.new_path); From 39a3422b4517cd7138f600a91dfb551e0614a28e Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:16:27 +0000 Subject: [PATCH 26/26] RPC: Add ASCII event support (#284) * RPC: SendAsciiEventRequest support * GUI: Ascii input for ByteInput * Sync protobuf submodule * Update changelog --- CHANGELOG.md | 1 + applications/services/rpc/rpc_gui.c | 34 +++++++++++++++++++++++++++++ assets/protobuf | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f47d67abbc..b9cba5555b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ - JS: - New `i2c` module (#259 by @jamisonderek) - New `spi` module (#272 by @jamisonderek) +- RPC: Added ASCII event support (#284 by @Willy-JL) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) - Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme) diff --git a/applications/services/rpc/rpc_gui.c b/applications/services/rpc/rpc_gui.c index 447156bc91..70c5f14632 100644 --- a/applications/services/rpc/rpc_gui.c +++ b/applications/services/rpc/rpc_gui.c @@ -269,6 +269,37 @@ static void rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); } +static void + rpc_system_gui_send_ascii_event_request_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_gui_send_ascii_event_request_tag); + furi_assert(context); + + FURI_LOG_D(TAG, "SendAsciiEvent"); + + RpcGuiSystem* rpc_gui = context; + RpcSession* session = rpc_gui->session; + furi_assert(session); + + bool is_valid = (request->content.gui_send_ascii_event_request.value <= 0xFF); + + if(!is_valid) { + rpc_send_and_release_empty( + session, request->command_id, PB_CommandStatus_ERROR_INVALID_PARAMETERS); + return; + } + + AsciiEvent event = { + .value = request->content.gui_send_ascii_event_request.value, + }; + + // Submit event + FuriPubSub* ascii_events = furi_record_open(RECORD_ASCII_EVENTS); + furi_pubsub_publish(ascii_events, &event); + furi_record_close(RECORD_ASCII_EVENTS); + rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); +} + static void rpc_system_gui_virtual_display_render_callback(Canvas* canvas, void* context) { furi_assert(canvas); furi_assert(context); @@ -459,6 +490,9 @@ void* rpc_system_gui_alloc(RpcSession* session) { rpc_handler.message_handler = rpc_system_gui_send_input_event_request_process; rpc_add_handler(session, PB_Main_gui_send_input_event_request_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_gui_send_ascii_event_request_process; + rpc_add_handler(session, PB_Main_gui_send_ascii_event_request_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_gui_start_virtual_display_process; rpc_add_handler(session, PB_Main_gui_start_virtual_display_request_tag, &rpc_handler); diff --git a/assets/protobuf b/assets/protobuf index b7d5881690..c255d71a90 160000 --- a/assets/protobuf +++ b/assets/protobuf @@ -1 +1 @@ -Subproject commit b7d5881690298de836df6305d4c71f96d5d34c61 +Subproject commit c255d71a90af202515deb7aaa51685d45a196152