From d0214c7b272d277fc5f3bded5e0d65adf1a63be1 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 23 Oct 2024 20:07:03 +0300 Subject: [PATCH 1/8] 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 70021968f..d2d65da4d 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 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 2/8] 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 f8f1be13f..5402bada7 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 7a774d324..83f9e604c 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 5c8844d22..2d6dae475 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 4bd4ccc31..22d04855d 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 67266b1fc..d400d0a33 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 aecd413be..c142bcddb 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 d2bf4a8f9..e93bbfad0 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 7d54cf9b9..cf66b6a44 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 000000000..c0f4d684d --- /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 4fbda5ef8..57c2662cd 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 d132f89f5..a55dae7d9 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 25abca4af..4924eea7e 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 000000000..8d72bc29c --- /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 3/8] 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 5faeb6d32..a1e023853 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 34639cdac..40ca98c30 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 000000000..810637b3b --- /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 4/8] ndef parser updates by luu176 and Willy-JL --- .../main/nfc/plugins/supported_cards/ndef.c | 1206 ++++++++++++----- 1 file changed, 882 insertions(+), 324 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index fe71915eb..820324421 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 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); - if(!force_hex && is_text(buf, len)) { - furi_string_cat_printf(str, "%.*s", len, buf); - } else { - for(uint8_t i = 0; i < len; i++) { - furi_string_cat_printf(str, "%02X ", buf[i]); +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); } } - furi_string_cat(str, "\n"); + if(force_hex) { + for(size_t i = 0; i < len; i++) { + uint8_t b; + if(!ndef_get(ndef, pos + i, 1, &b)) return false; + furi_string_cat_printf(ndef->output, "%02X ", b); + } + } + furi_string_cat(ndef->output, "\n"); + return true; } +static void + ndef_print(Ndef* ndef, const char* prefix, const void* buf, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + if(!force_hex && is_text(buf, len)) { + furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); + } else { + for(size_t i = 0; i < len; i++) { + furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); + } + } + furi_string_cat(ndef->output, "\n"); +} + +// ---=== payload parsing ===--- + static inline uint8_t hex_to_int(char c) { if(c >= '0' && c <= '9') return c - '0'; if(c >= 'A' && c <= 'F') return c - 'A' + 10; @@ -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]; + } + 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 + } } - 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 + // 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; + } + } - // 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; + // Print static data as-is + furi_string_cat_printf(ndef->output, "%s\n", type); + if(prepend) { + furi_string_cat(ndef->output, prepend); + } + + // Print URI one char at a time and perform URL decode + while(len) { + char c; + if(!ndef_get(ndef, pos++, 1, &c)) return false; + len--; + if(c != '%' || len < 2) { // Not encoded, or not enough remaining text for encoded char + furi_string_push_back(ndef->output, c); + continue; + } + char enc[2]; + if(!ndef_get(ndef, pos, 2, enc)) return false; + enc[0] = toupper(enc[0]); + enc[1] = toupper(enc[1]); + // Only consume and print these 2 characters if they're valid URL encoded + // Otherwise they're processed in next iterations and we output the % char + if(((enc[0] >= 'A' && enc[0] <= 'F') || (enc[0] >= '0' && enc[0] <= '9')) && + ((enc[1] >= 'A' && enc[1] <= 'F') || (enc[1] >= '0' && enc[1] <= '9'))) { + pos += 2; + len -= 2; + c = url_decode_char(enc); + } + furi_string_push_back(ndef->output, c); + } + + return true; +} + +static bool ndef_parse_text(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "Text\n"); + if(!ndef_dump(ndef, NULL, pos + 3, len - 3, false)) return false; + return true; +} + +static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "BT MAC\n"); + if(len != 8) return false; + if(!ndef_dump(ndef, NULL, pos + 2, len - 2, true)) return false; + return true; +} + +static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { + // We hide redundant tags the user is probably not interested in. + // Would be easier with FuriString checks for start_with() and end_with() + // but to do that would waste lots of RAM on a cloned string buffer + // so instead we just look for these markers at start/end and shift + // pos and len then use ndef_dump() to output one char at a time. + // Results in minimal stack and no heap usage at all. + static const char* const begin_tag = "BEGIN:VCARD"; + static const uint8_t begin_len = strlen(begin_tag); + static const char* const version_tag = "VERSION:"; + static const uint8_t version_len = strlen(version_tag); + static const char* const end_tag = "END:VCARD"; + static const uint8_t end_len = strlen(end_tag); + char tmp[13] = {0}; // Enough for BEGIN:VCARD\r\n + uint8_t skip = 0; + + // Skip BEGIN tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(begin_tag, tmp, begin_len) == 0) { + skip = begin_len; + if(tmp[skip] == '\r') skip++; + if(tmp[skip] == '\n') skip++; + pos += skip; + len -= skip; + } + } + + // Skip VERSION tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(version_tag, tmp, version_len) == 0) { + skip = version_len; + while(skip < len) { + if(!ndef_get(ndef, pos + skip, 1, &tmp[0])) return false; + skip++; + if(tmp[0] == '\n') break; + } + pos += skip; + len -= skip; + } + } + + // Skip END tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos + len - sizeof(tmp), sizeof(tmp), tmp)) return false; + // Read more than length of END tag and check multiple offsets, might have some padding after + // Worst case: there is END:VCARD\r\n\r\n which is same length as tmp buffer (13) + // Not sure if this is in spec but might aswell check + static const uint8_t offsets = sizeof(tmp) - end_len + 1; + for(uint8_t offset = 0; offset < offsets; offset++) { + if(strncmp(end_tag, tmp + offset, end_len) == 0) { + skip = sizeof(tmp) - offset; + len -= skip; + break; } } - 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; - } + furi_string_cat(ndef->output, "Contact\n"); + ndef_dump(ndef, NULL, pos, len, false); - furi_string_cat_printf(str, "%s\n", type); - print_data(str, NULL, (uint8_t*)uri, decoded_len, false); - - free(uri_buf); + 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 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 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')); - } - 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); - } - } - - 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); -} - -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; + 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(i + cfg_len > start_position + field_len) { - return; + 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, - const uint8_t* payload, - uint32_t payload_len) { - if(!payload_len) { - furi_string_cat(str, "Empty\n"); - return; + uint8_t type_len); +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed); + +bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, + const char* type, + uint8_t type_len) { + FURI_LOG_D(TAG, "payload type: %.*s len: %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] - if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { - parse_ndef_bt(str, payload, payload_len); - } else if(strncmp("text/vcard", type, type_len) == 0) { - parse_ndef_vcard(str, payload, payload_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); - } - 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, "Well-known Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfMediaType: + if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { + return ndef_parse_bt(ndef, pos, len); + } else if(strncmp("text/vcard", type, type_len) == 0) { + return ndef_parse_vcard(ndef, pos, len); + } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { + return ndef_parse_wifi(ndef, pos, len); + } + // Dump data without parsing + furi_string_cat(ndef->output, "Unknown\n"); + ndef_print(ndef, "Media Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfEmpty: + case NdefTnfAbsoluteUri: + case NdefTnfExternalType: + case NdefTnfUnknown: + case NdefTnfUnchanged: + case NdefTnfReserved: + default: + // Dump data without parsing + furi_string_cat(ndef->output, "Unsupported\n"); + ndef_print(ndef, "Type name format", &tnf, 1, true); + ndef_print(ndef, "Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; } } -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; + NDEF_TITLE(device, parsed_data); - // 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 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(message_num++ == 0) { - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); - } + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } - const uint8_t* message_end = cur + len; - cur = parse_ndef_message(parsed_data, message_num, cur, message_end); - if(cur != message_end) cur = end; + return parsed > 0; +} - break; - } +#elif NDEF_PROTO == NDEF_PROTO_MFC - case 0xFE: // TLV end - cur = end; - if(message_num != 0) parsed = true; - 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; - case 0x00: // Padding, has no length, skip - break; +// 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}; - 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; +static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); - default: // Unknown, bail to avoid problems - cur = end; - break; + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + // Check card type can contain NDEF + if(data->type != MfClassicType1k && data->type != MfClassicType4k && + data->type != MfClassicTypeMini) { + return false; + } + + // Check MADs for what sectors contain NDEF data AIDs + bool sectors_with_ndef[MF_CLASSIC_TOTAL_SECTORS_MAX] = {0}; + const size_t sector_count = mf_classic_get_total_sectors_num(data->type); + const struct { + size_t block; + uint8_t aid_count; + } mads[2] = { + {1, 15}, + {64, 23}, + }; + for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { + const size_t block = mads[mad].block; + const size_t sector = mf_classic_get_sector_by_block(block); + if(sector_count <= sector) break; // Skip this MAD if not present + // Check MAD key + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector); + const uint64_t sector_key_a = bit_lib_bytes_to_num_be( + sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); + if(sector_key_a != mad_key) return false; + // Find NDEF AIDs + for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { + const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; + if(memcmp(aid, ndef_aid, AID_SIZE) == 0) { + sectors_with_ndef[aid_index + 1] = true; } } + } + + NDEF_TITLE(device, parsed_data); + + // Calculate how large the data space is, so excluding sector trailers and MAD2. + // Makes sure we stay within this card's actual content when parsing. + // First 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 32 sectors correspond to 93 (31*3) data blocks. + // Last 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 8 sectors correspond to 120 (8*15) data blocks. + size_t data_size; + if(sector_count > 32) { + data_size = 93 + (sector_count - 32) * 15; + } else { + data_size = sector_count * 3; + if(sector_count >= 16) { + data_size -= 3; // Skip MAD2 + } + } + data_size *= MF_CLASSIC_BLOCK_SIZE; + + Ndef ndef = { + .output = parsed_data, + .mfc = + { + .blocks = data->block, + .size = data_size, + }, + }; + size_t total_parsed = 0; + + for(size_t sector = 0; sector < sector_count; sector++) { + if(!sectors_with_ndef[sector]) continue; + FURI_LOG_D(TAG, "sector: %d", sector); + size_t string_prev = furi_string_size(parsed_data); + + // Convert real sector number to data block number + // to skip sector trailers and MAD2 + size_t data_block; + if(sector < 32) { + data_block = sector * 3; + if(sector >= 16) { + data_block -= 3; // Skip MAD2 + } + } else { + data_block = 93 + (sector - 32) * 15; + } + FURI_LOG_D(TAG, "data_block: %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 5/8] 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 a78a35950..3eb24096b 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 6/8] 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 c38140de2..9f2491691 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 7/8] upd changelog --- CHANGELOG.md | 67 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4cd0014..4665f45a6 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 8/8] 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 4665f45a6..ff636a393 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 ac4c442f8..61fe9ca7b 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)