From 0103b8c7c55a73384b272acf0bb26f86121c7ceb Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 1 Apr 2025 18:50:08 +0400 Subject: [PATCH 1/3] [FL-3961] New JS value destructuring (#4135) * js: value destructuring and tests * js: temporary fix to see size impact * js_val: reduce code size 1 * i may be stupid. * test: js_value args * Revert "js: temporary fix to see size impact" This reverts commit f51d726dbafc4300d3552020de1c3b8f9ecd3ac1. * pvs: silence warnings * style: formatting * pvs: silence warnings? * pvs: silence warnings?? * js_value: redesign declaration types for less code * js: temporary fix to see size impact * style: formatting * pvs: fix helpful warnings * js_value: reduce .rodata size * pvs: fix helpful warning * js_value: reduce code size 1 * fix build error * style: format * Revert "js: temporary fix to see size impact" This reverts commit d6a46f01794132e882e03fd273dec24386a4f8ba. * style: format --------- Co-authored-by: hedger --- .../debug/unit_tests/tests/js/js_test.c | 307 ++++++++++++++++++ applications/debug/unit_tests/tests/minunit.h | 4 + .../debug/unit_tests/unit_test_api_table_i.h | 13 + applications/system/js_app/application.fam | 1 + applications/system/js_app/js_modules.h | 1 + applications/system/js_app/js_value.c | 291 +++++++++++++++++ applications/system/js_app/js_value.h | 212 ++++++++++++ 7 files changed, 829 insertions(+) create mode 100644 applications/system/js_app/js_value.c create mode 100644 applications/system/js_app/js_value.h diff --git a/applications/debug/unit_tests/tests/js/js_test.c b/applications/debug/unit_tests/tests/js/js_test.c index af590e899..dd695a3a1 100644 --- a/applications/debug/unit_tests/tests/js/js_test.c +++ b/applications/debug/unit_tests/tests/js/js_test.c @@ -6,9 +6,12 @@ #include #include +#include #include +#define TAG "JsUnitTests" + #define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js") typedef enum { @@ -73,7 +76,311 @@ MU_TEST(js_test_storage) { js_test_run(JS_SCRIPT_PATH("storage")); } +static void js_value_test_compatibility_matrix(struct mjs* mjs) { + static const JsValueType types[] = { + JsValueTypeAny, + JsValueTypeAnyArray, + JsValueTypeAnyObject, + JsValueTypeFunction, + JsValueTypeRawPointer, + JsValueTypeInt32, + JsValueTypeDouble, + JsValueTypeString, + JsValueTypeBool, + }; + + mjs_val_t values[] = { + mjs_mk_undefined(), + mjs_mk_foreign(mjs, (void*)0xDEADBEEF), + mjs_mk_array(mjs), + mjs_mk_object(mjs), + mjs_mk_number(mjs, 123.456), + mjs_mk_string(mjs, "test", ~0, false), + mjs_mk_boolean(mjs, true), + }; + +// for proper matrix formatting and better readability +#define YES true +#define NO_ false + static const bool success_matrix[COUNT_OF(types)][COUNT_OF(values)] = { + // types: + {YES, YES, YES, YES, YES, YES, YES}, // any + {NO_, NO_, YES, NO_, NO_, NO_, NO_}, // array + {NO_, NO_, YES, YES, NO_, NO_, NO_}, // obj + {NO_, NO_, NO_, NO_, NO_, NO_, NO_}, // fn + {NO_, YES, NO_, NO_, NO_, NO_, NO_}, // ptr + {NO_, NO_, NO_, NO_, YES, NO_, NO_}, // int32 + {NO_, NO_, NO_, NO_, YES, NO_, NO_}, // double + {NO_, NO_, NO_, NO_, NO_, YES, NO_}, // str + {NO_, NO_, NO_, NO_, NO_, NO_, YES}, // bool + // + //und ptr arr obj num str bool <- values + }; +#undef NO_ +#undef YES + + for(size_t i = 0; i < COUNT_OF(types); i++) { + for(size_t j = 0; j < COUNT_OF(values); j++) { + const JsValueDeclaration declaration = { + .type = types[i], + .n_children = 0, + }; + // we only care about the status, not the result. double has the largest size out of + // all the results + uint8_t result[sizeof(double)]; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&declaration), + JsValueParseFlagNone, + &status, + &values[j], + result); + if((status == JsValueParseStatusOk) != success_matrix[i][j]) { + FURI_LOG_E(TAG, "type %zu, value %zu", i, j); + mu_fail("see serial logs"); + } + } + } +} + +static void js_value_test_literal(struct mjs* mjs) { + static const JsValueType types[] = { + JsValueTypeAny, + JsValueTypeAnyArray, + JsValueTypeAnyObject, + }; + + mjs_val_t values[] = { + mjs_mk_undefined(), + mjs_mk_array(mjs), + mjs_mk_object(mjs), + }; + + mu_assert_int_eq(COUNT_OF(types), COUNT_OF(values)); + for(size_t i = 0; i < COUNT_OF(types); i++) { + const JsValueDeclaration declaration = { + .type = types[i], + .n_children = 0, + }; + mjs_val_t result; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&declaration), + JsValueParseFlagNone, + &status, + &values[i], + &result); + mu_assert_int_eq(JsValueParseStatusOk, status); + mu_assert(result == values[i], "wrong result"); + } +} + +static void js_value_test_primitive( + struct mjs* mjs, + JsValueType type, + const void* c_value, + size_t c_value_size, + mjs_val_t js_val) { + const JsValueDeclaration declaration = { + .type = type, + .n_children = 0, + }; + uint8_t result[c_value_size]; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&declaration), + JsValueParseFlagNone, + &status, + &js_val, + result); + mu_assert_int_eq(JsValueParseStatusOk, status); + if(type == JsValueTypeString) { + const char* result_str = *(const char**)&result; + mu_assert_string_eq(c_value, result_str); + } else { + mu_assert_mem_eq(c_value, result, c_value_size); + } +} + +static void js_value_test_primitives(struct mjs* mjs) { + int32_t i32 = 123; + js_value_test_primitive(mjs, JsValueTypeInt32, &i32, sizeof(i32), mjs_mk_number(mjs, i32)); + + double dbl = 123.456; + js_value_test_primitive(mjs, JsValueTypeDouble, &dbl, sizeof(dbl), mjs_mk_number(mjs, dbl)); + + const char* str = "test"; + js_value_test_primitive( + mjs, JsValueTypeString, str, strlen(str) + 1, mjs_mk_string(mjs, str, ~0, false)); + + bool boolean = true; + js_value_test_primitive( + mjs, JsValueTypeBool, &boolean, sizeof(boolean), mjs_mk_boolean(mjs, boolean)); +} + +static uint32_t + js_value_test_enum(struct mjs* mjs, const JsValueDeclaration* decl, const char* value) { + mjs_val_t str = mjs_mk_string(mjs, value, ~0, false); + uint32_t result; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, JS_VALUE_PARSE_SOURCE_VALUE(decl), JsValueParseFlagNone, &status, &str, &result); + if(status != JsValueParseStatusOk) return 0; + return result; +} + +static void js_value_test_enums(struct mjs* mjs) { + static const JsValueEnumVariant enum_1_variants[] = { + {"variant 1", 1}, + {"variant 2", 2}, + {"variant 3", 3}, + }; + static const JsValueDeclaration enum_1 = JS_VALUE_ENUM(uint32_t, enum_1_variants); + + static const JsValueEnumVariant enum_2_variants[] = { + {"read", 4}, + {"write", 8}, + }; + static const JsValueDeclaration enum_2 = JS_VALUE_ENUM(uint32_t, enum_2_variants); + + mu_assert_int_eq(1, js_value_test_enum(mjs, &enum_1, "variant 1")); + mu_assert_int_eq(2, js_value_test_enum(mjs, &enum_1, "variant 2")); + mu_assert_int_eq(3, js_value_test_enum(mjs, &enum_1, "variant 3")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_1, "not a thing")); + + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 1")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 2")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 3")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "not a thing")); + mu_assert_int_eq(4, js_value_test_enum(mjs, &enum_2, "read")); + mu_assert_int_eq(8, js_value_test_enum(mjs, &enum_2, "write")); +} + +static void js_value_test_object(struct mjs* mjs) { + static const JsValueDeclaration int_decl = JS_VALUE_SIMPLE(JsValueTypeInt32); + + static const JsValueDeclaration str_decl = JS_VALUE_SIMPLE(JsValueTypeString); + + static const JsValueEnumVariant enum_variants[] = { + {"variant 1", 1}, + {"variant 2", 2}, + {"variant 3", 3}, + }; + static const JsValueDeclaration enum_decl = JS_VALUE_ENUM(uint32_t, enum_variants); + + static const JsValueObjectField fields[] = { + {"int", &int_decl}, + {"str", &str_decl}, + {"enum", &enum_decl}, + }; + static const JsValueDeclaration object_decl = JS_VALUE_OBJECT(fields); + + mjs_val_t object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, object) { + JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false)); + JS_FIELD("int", mjs_mk_number(mjs, 123)); + JS_FIELD("enum", mjs_mk_string(mjs, "variant 2", ~0, false)); + } + + const char* result_str; + int32_t result_int; + uint32_t result_enum; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&object_decl), + JsValueParseFlagNone, + &status, + &object, + &result_int, + &result_str, + &result_enum); + mu_assert_int_eq(JsValueParseStatusOk, status); + mu_assert_string_eq("Helloooo!", result_str); + mu_assert_int_eq(123, result_int); + mu_assert_int_eq(2, result_enum); +} + +static void js_value_test_default(struct mjs* mjs) { + static const JsValueDeclaration int_decl = + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 123); + static const JsValueDeclaration str_decl = JS_VALUE_SIMPLE(JsValueTypeString); + + static const JsValueObjectField fields[] = { + {"int", &int_decl}, + {"str", &str_decl}, + }; + static const JsValueDeclaration object_decl = JS_VALUE_OBJECT(fields); + + mjs_val_t object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, object) { + JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false)); + JS_FIELD("int", mjs_mk_undefined()); + } + + const char* result_str; + int32_t result_int; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&object_decl), + JsValueParseFlagNone, + &status, + &object, + &result_int, + &result_str); + mu_assert_string_eq("Helloooo!", result_str); + mu_assert_int_eq(123, result_int); +} + +static void js_value_test_args_fn(struct mjs* mjs) { + static const JsValueDeclaration arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments args = JS_VALUE_ARGS(arg_list); + + int32_t a, b, c; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &args, &a, &b, &c); + + mu_assert_int_eq(123, a); + mu_assert_int_eq(456, b); + mu_assert_int_eq(-420, c); +} + +static void js_value_test_args(struct mjs* mjs) { + mjs_val_t function = MJS_MK_FN(js_value_test_args_fn); + + mjs_val_t result; + mjs_val_t args[] = { + mjs_mk_number(mjs, 123), + mjs_mk_number(mjs, 456), + mjs_mk_number(mjs, -420), + }; + mu_assert_int_eq( + MJS_OK, mjs_apply(mjs, &result, function, MJS_UNDEFINED, COUNT_OF(args), args)); +} + +MU_TEST(js_value_test) { + struct mjs* mjs = mjs_create(NULL); + + js_value_test_compatibility_matrix(mjs); + js_value_test_literal(mjs); + js_value_test_primitives(mjs); + js_value_test_enums(mjs); + js_value_test_object(mjs); + js_value_test_default(mjs); + js_value_test_args(mjs); + + mjs_destroy(mjs); +} + MU_TEST_SUITE(test_js) { + MU_RUN_TEST(js_value_test); MU_RUN_TEST(js_test_basic); MU_RUN_TEST(js_test_math); MU_RUN_TEST(js_test_event_loop); diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index 9ca3bb403..c854c4673 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -396,6 +396,8 @@ void minunit_printf_warning(const char* format, ...); return; \ } else { minunit_print_progress(); }) +//-V:mu_assert_string_eq:526, 547 + #define mu_assert_string_eq(expected, result) \ MU__SAFE_BLOCK( \ const char* minunit_tmp_e = expected; const char* minunit_tmp_r = result; \ @@ -416,6 +418,8 @@ void minunit_printf_warning(const char* format, ...); return; \ } else { minunit_print_progress(); }) +//-V:mu_assert_mem_eq:526 + #define mu_assert_mem_eq(expected, result, size) \ MU__SAFE_BLOCK( \ const void* minunit_tmp_e = expected; const void* minunit_tmp_r = result; \ diff --git a/applications/debug/unit_tests/unit_test_api_table_i.h b/applications/debug/unit_tests/unit_test_api_table_i.h index 10b089022..4f0e4dec9 100644 --- a/applications/debug/unit_tests/unit_test_api_table_i.h +++ b/applications/debug/unit_tests/unit_test_api_table_i.h @@ -8,6 +8,7 @@ #include #include #include +#include static constexpr auto unit_tests_api_table = sort(create_array_t( API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)), @@ -38,4 +39,16 @@ static constexpr auto unit_tests_api_table = sort(create_array_t( JsThread*, (const char* script_path, JsThreadCallback callback, void* context)), API_METHOD(js_thread_stop, void, (JsThread * worker)), + API_METHOD(js_value_buffer_size, size_t, (const JsValueParseDeclaration declaration)), + API_METHOD( + js_value_parse, + JsValueParseStatus, + (struct mjs * mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...)), API_VARIABLE(PB_Main_msg, PB_Main_msg_t))); diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index db1521b9d..66ec221ec 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -11,6 +11,7 @@ App( "js_app.c", "js_modules.c", "js_thread.c", + "js_value.c", "plugin_api/app_api_table.cpp", "views/console_view.c", "modules/js_flipper.c", diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 892e43d4e..fb1cca915 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -2,6 +2,7 @@ #include #include "js_thread_i.h" +#include "js_value.h" #include #include #include diff --git a/applications/system/js_app/js_value.c b/applications/system/js_app/js_value.c new file mode 100644 index 000000000..6ce1cf37a --- /dev/null +++ b/applications/system/js_app/js_value.c @@ -0,0 +1,291 @@ +#include "js_value.h" +#include + +#ifdef APP_UNIT_TESTS +#define JS_VAL_DEBUG +#endif + +size_t js_value_buffer_size(const JsValueParseDeclaration declaration) { + if(declaration.source == JsValueParseSourceValue) { + const JsValueDeclaration* value_decl = declaration.value_decl; + JsValueType type = value_decl->type & JsValueTypeMask; + + if(type == JsValueTypeString) return 1; + + if(type == JsValueTypeObject) { + size_t total = 0; + for(size_t i = 0; i < value_decl->n_children; i++) + total += js_value_buffer_size( + JS_VALUE_PARSE_SOURCE_VALUE(value_decl->object_fields[i].value)); + return total; + } + + return 0; + + } else { + const JsValueArguments* arg_decl = declaration.argument_decl; + size_t total = 0; + for(size_t i = 0; i < arg_decl->n_children; i++) + total += js_value_buffer_size(JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i])); + return total; + } +} + +static size_t js_value_resulting_c_values_count(const JsValueParseDeclaration declaration) { + if(declaration.source == JsValueParseSourceValue) { + const JsValueDeclaration* value_decl = declaration.value_decl; + JsValueType type = value_decl->type & JsValueTypeMask; + + if(type == JsValueTypeObject) { + size_t total = 0; + for(size_t i = 0; i < value_decl->n_children; i++) + total += js_value_resulting_c_values_count( + JS_VALUE_PARSE_SOURCE_VALUE(value_decl->object_fields[i].value)); + return total; + } + + return 1; + + } else { + const JsValueArguments* arg_decl = declaration.argument_decl; + size_t total = 0; + for(size_t i = 0; i < arg_decl->n_children; i++) + total += js_value_resulting_c_values_count( + JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i])); + return total; + } +} + +#define PREPEND_JS_ERROR_AND_RETURN(mjs, flags, ...) \ + do { \ + if((flags) & JsValueParseFlagReturnOnError) \ + mjs_prepend_errorf((mjs), MJS_BAD_ARGS_ERROR, __VA_ARGS__); \ + return JsValueParseStatusJsError; \ + } while(0) + +#define PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, type) \ + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected %s", type) + +static void js_value_assign_enum_val(void* destination, JsValueType type_w_flags, uint32_t value) { + if(type_w_flags & JsValueTypeEnumSize1) { + *(uint8_t*)destination = value; + } else if(type_w_flags & JsValueTypeEnumSize2) { + *(uint16_t*)destination = value; + } else if(type_w_flags & JsValueTypeEnumSize4) { + *(uint32_t*)destination = value; + } +} + +static bool js_value_is_null_or_undefined(mjs_val_t* val_ptr) { + return mjs_is_null(*val_ptr) || mjs_is_undefined(*val_ptr); +} + +static bool js_value_maybe_assign_default( + const JsValueDeclaration* declaration, + mjs_val_t* val_ptr, + void* destination, + size_t size) { + if((declaration->type & JsValueTypePermitNull) && js_value_is_null_or_undefined(val_ptr)) { + memcpy(destination, &declaration->default_value, size); + return true; + } + return false; +} + +typedef int (*MjsTypecheckFn)(mjs_val_t value); + +static JsValueParseStatus js_value_parse_literal( + struct mjs* mjs, + JsValueParseFlag flags, + mjs_val_t* destination, + mjs_val_t* source, + MjsTypecheckFn typecheck, + const char* type_name) { + if(!typecheck(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, type_name); + *destination = *source; + return JsValueParseStatusOk; +} + +static JsValueParseStatus js_value_parse_va( + struct mjs* mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* source, + mjs_val_t* buffer, + size_t* buffer_index, + va_list* out_pointers) { + if(declaration.source == JsValueParseSourceArguments) { + const JsValueArguments* arg_decl = declaration.argument_decl; + + for(size_t i = 0; i < arg_decl->n_children; i++) { + mjs_val_t arg_val = mjs_arg(mjs, i); + JsValueParseStatus status = js_value_parse_va( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i]), + flags, + &arg_val, + buffer, + buffer_index, + out_pointers); + if(status != JsValueParseStatusOk) return status; + } + + return JsValueParseStatusOk; + } + + const JsValueDeclaration* value_decl = declaration.value_decl; + JsValueType type_w_flags = value_decl->type; + JsValueType type_noflags = type_w_flags & JsValueTypeMask; + bool is_null_but_allowed = (type_w_flags & JsValueTypePermitNull) && + js_value_is_null_or_undefined(source); + + void* destination = NULL; + if(type_noflags != JsValueTypeObject) destination = va_arg(*out_pointers, void*); + + switch(type_noflags) { + // Literal terms + case JsValueTypeAny: + *(mjs_val_t*)destination = *source; + break; + case JsValueTypeAnyArray: + return js_value_parse_literal(mjs, flags, destination, source, mjs_is_array, "array"); + case JsValueTypeAnyObject: + return js_value_parse_literal(mjs, flags, destination, source, mjs_is_object, "array"); + case JsValueTypeFunction: + return js_value_parse_literal( + mjs, flags, destination, source, mjs_is_function, "function"); + + // Primitive types + case JsValueTypeRawPointer: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(void*))) break; + if(!mjs_is_foreign(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "pointer"); + *(void**)destination = mjs_get_ptr(mjs, *source); + break; + } + case JsValueTypeInt32: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(int32_t))) break; + if(!mjs_is_number(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "number"); + *(int32_t*)destination = mjs_get_int32(mjs, *source); + break; + } + case JsValueTypeDouble: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(double))) break; + if(!mjs_is_number(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "number"); + *(double*)destination = mjs_get_double(mjs, *source); + break; + } + case JsValueTypeBool: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(bool))) break; + if(!mjs_is_boolean(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "bool"); + *(bool*)destination = mjs_get_bool(mjs, *source); + break; + } + case JsValueTypeString: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(const char*))) + break; + if(!mjs_is_string(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "string"); + buffer[*buffer_index] = *source; + *(const char**)destination = mjs_get_string(mjs, &buffer[*buffer_index], NULL); + (*buffer_index)++; + break; + } + + // Types with children + case JsValueTypeEnum: { + if(is_null_but_allowed) { + js_value_assign_enum_val( + destination, type_w_flags, value_decl->default_value.enum_val); + + } else if(mjs_is_string(*source)) { + const char* str = mjs_get_string(mjs, source, NULL); + furi_check(str); + + bool match_found = false; + for(size_t i = 0; i < value_decl->n_children; i++) { + const JsValueEnumVariant* variant = &value_decl->enum_variants[i]; + if(strcmp(str, variant->string_value) == 0) { + js_value_assign_enum_val(destination, type_w_flags, variant->num_value); + match_found = true; + break; + } + } + + if(!match_found) + PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "one of permitted strings"); + + } else { + PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "string"); + } + break; + } + + case JsValueTypeObject: { + if(!(is_null_but_allowed || mjs_is_object(*source))) + PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "object"); + for(size_t i = 0; i < value_decl->n_children; i++) { + const JsValueObjectField* field = &value_decl->object_fields[i]; + mjs_val_t field_val = mjs_get(mjs, *source, field->field_name, ~0); + JsValueParseStatus status = js_value_parse_va( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(field->value), + flags, + &field_val, + buffer, + buffer_index, + out_pointers); + if(status != JsValueParseStatusOk) + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "field %s: ", field->field_name); + } + break; + } + + case JsValueTypeMask: + case JsValueTypeEnumSize1: + case JsValueTypeEnumSize2: + case JsValueTypeEnumSize4: + case JsValueTypePermitNull: + furi_crash(); + } + + return JsValueParseStatusOk; +} + +JsValueParseStatus js_value_parse( + struct mjs* mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...) { + furi_check(mjs); + furi_check(buffer); + + if(declaration.source == JsValueParseSourceValue) { + furi_check(source); + furi_check(declaration.value_decl); + } else { + furi_check(source == NULL); + furi_check(declaration.argument_decl); + } + +#ifdef JS_VAL_DEBUG + furi_check(buf_size == js_value_buffer_size(declaration)); + furi_check(n_c_vals == js_value_resulting_c_values_count(declaration)); +#else + UNUSED(js_value_resulting_c_values_count); +#endif + + va_list out_pointers; + va_start(out_pointers, n_c_vals); + + size_t buffer_index = 0; + JsValueParseStatus status = + js_value_parse_va(mjs, declaration, flags, source, buffer, &buffer_index, &out_pointers); + furi_check(buffer_index <= buf_size); + + va_end(out_pointers); + + return status; +} diff --git a/applications/system/js_app/js_value.h b/applications/system/js_app/js_value.h new file mode 100644 index 000000000..765bcb3bb --- /dev/null +++ b/applications/system/js_app/js_value.h @@ -0,0 +1,212 @@ +#pragma once + +#include +#include "js_modules.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + // literal types + JsValueTypeAny, // Date: Tue, 1 Apr 2025 16:16:14 +0000 Subject: [PATCH 2/3] Docs: Fix doxygen references from PR 4168 (#4169) * Docs: Fix doxygen references from PR 4168 * Update JS GUI adapter table --------- Co-authored-by: hedger --- documentation/doxygen/js.dox | 1 + documentation/js/js_gui.md | 47 +++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/documentation/doxygen/js.dox b/documentation/doxygen/js.dox index 7abde9389..fadf0c023 100644 --- a/documentation/doxygen/js.dox +++ b/documentation/doxygen/js.dox @@ -17,6 +17,7 @@ Flipper Zero's built-in JavaScript engine enables you to run lightweight scripts - @subpage js_badusb — This module allows you to emulate a standard USB keyboard - @subpage js_event_loop — The module for easy event-based developing +- @subpage js_flipper — This module allows to query device information - @subpage js_gpio — This module allows you to control GPIO pins - @subpage js_gui — This module allows you to use GUI (graphical user interface) - @subpage js_math — This module contains mathematical methods and constants diff --git a/documentation/js/js_gui.md b/documentation/js/js_gui.md index afca1434e..b04255008 100644 --- a/documentation/js/js_gui.md +++ b/documentation/js/js_gui.md @@ -10,12 +10,15 @@ let gui = require("gui"); GUI module has several submodules: -- @subpage js_gui__submenu — Displays a scrollable list of clickable textual entries -- @subpage js_gui__loading — Displays an animated hourglass icon -- @subpage js_gui__empty_screen — Just empty screen -- @subpage js_gui__text_input — Keyboard-like text input -- @subpage js_gui__text_box — Simple multiline text box +- @subpage js_gui__byte_input — Keyboard-like hex input - @subpage js_gui__dialog — Dialog with up to 3 options +- @subpage js_gui__empty_screen — Just empty screen +- @subpage js_gui__file_picker — Displays a file selection prompt +- @subpage js_gui__icon — Retrieves and loads icons for use in GUI +- @subpage js_gui__loading — Displays an animated hourglass icon +- @subpage js_gui__submenu — Displays a scrollable list of clickable textual entries +- @subpage js_gui__text_box — Simple multiline text box +- @subpage js_gui__text_input — Keyboard-like text input - @subpage js_gui__widget — Displays a combination of custom elements on one screen --- @@ -38,23 +41,23 @@ always access the canvas through a viewport. In Flipper's terminology, a "View" is a fullscreen design element that assumes control over the entire viewport and all input events. Different types of views are available (not all of which are unfortunately currently implemented in JS): -| View | Has JS adapter? | -|----------------------|------------------| -| `button_menu` | ❌ | -| `button_panel` | ❌ | -| `byte_input` | ❌ | -| `dialog_ex` | ✅ (as `dialog`) | -| `empty_screen` | ✅ | -| `file_browser` | ❌ | -| `loading` | ✅ | -| `menu` | ❌ | -| `number_input` | ❌ | -| `popup` | ❌ | -| `submenu` | ✅ | -| `text_box` | ✅ | -| `text_input` | ✅ | -| `variable_item_list` | ❌ | -| `widget` | ❌ | +| View | Has JS adapter? | +|----------------------|-----------------------| +| `button_menu` | ❌ | +| `button_panel` | ❌ | +| `byte_input` | ✅ | +| `dialog_ex` | ✅ (as `dialog`) | +| `empty_screen` | ✅ | +| `file_browser` | ✅ (as `file_picker`) | +| `loading` | ✅ | +| `menu` | ❌ | +| `number_input` | ❌ | +| `popup` | ❌ | +| `submenu` | ✅ | +| `text_box` | ✅ | +| `text_input` | ✅ | +| `variable_item_list` | ❌ | +| `widget` | ✅ | In JS, each view has its own set of properties (or just "props"). The programmer can manipulate these properties in two ways: From 5786066512f65a3fe32072e8601086e71840c164 Mon Sep 17 00:00:00 2001 From: Eric Betts Date: Tue, 1 Apr 2025 11:37:40 -0700 Subject: [PATCH 3/3] BLE advertising improvements (#4151) * Support longer advertised BLE UUID * BLE: support manufacturer data * Don't pair when GapPairingNone * Add PR feedback --------- Co-authored-by: hedger --- lib/ble_profile/extra_profiles/hid_profile.c | 5 +- targets/f7/ble_glue/gap.c | 46 ++++++++++++++++--- targets/f7/ble_glue/gap.h | 8 +++- targets/f7/ble_glue/profiles/serial_profile.c | 10 +++- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/ble_profile/extra_profiles/hid_profile.c b/lib/ble_profile/extra_profiles/hid_profile.c index f559a741a..ea90d3114 100644 --- a/lib/ble_profile/extra_profiles/hid_profile.c +++ b/lib/ble_profile/extra_profiles/hid_profile.c @@ -380,7 +380,10 @@ bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta) #define CONNECTION_INTERVAL_MAX (0x24) static GapConfig template_config = { - .adv_service_uuid = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, + .adv_service = { + .UUID_Type = UUID_TYPE_16, + .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, + }, .appearance_char = GAP_APPEARANCE_KEYBOARD, .bonding_mode = true, .pairing_method = GapPairingPinCodeVerifyYesNo, diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 732440ccf..1fe898ea9 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -23,6 +23,8 @@ typedef struct { uint16_t connection_handle; uint8_t adv_svc_uuid_len; uint8_t adv_svc_uuid[20]; + uint8_t mfg_data_len; + uint8_t mfg_data[20]; char* adv_name; } GapSvc; @@ -198,8 +200,10 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { gap->service.connection_handle = event->Connection_Handle; gap_verify_connection_parameters(gap); - // Start pairing by sending security request - aci_gap_slave_security_req(event->Connection_Handle); + if(gap->config->pairing_method != GapPairingNone) { + // Start pairing by sending security request + aci_gap_slave_security_req(event->Connection_Handle); + } } break; default: @@ -321,6 +325,14 @@ static void set_advertisment_service_uid(uint8_t* uid, uint8_t uid_len) { gap->service.adv_svc_uuid_len += uid_len; } +static void set_manufacturer_data(uint8_t* mfg_data, uint8_t mfg_data_len) { + furi_check(mfg_data_len < sizeof(gap->service.mfg_data) - 2); + gap->service.mfg_data[0] = mfg_data_len + 1; + gap->service.mfg_data[1] = AD_TYPE_MANUFACTURER_SPECIFIC_DATA; + memcpy(&gap->service.mfg_data[gap->service.mfg_data_len], mfg_data, mfg_data_len); + gap->service.mfg_data_len += mfg_data_len; +} + static void gap_init_svc(Gap* gap) { tBleStatus status; uint32_t srd_bd_addr[2]; @@ -440,6 +452,11 @@ static void gap_advertise_start(GapState new_state) { FURI_LOG_D(TAG, "set_non_discoverable success"); } } + + if(gap->service.mfg_data_len > 0) { + hci_le_set_scan_response_data(gap->service.mfg_data_len, gap->service.mfg_data); + } + // Configure advertising status = aci_gap_set_discoverable( ADV_IND, @@ -550,11 +567,26 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { gap->is_secure = false; gap->negotiation_round = 0; - uint8_t adv_service_uid[2]; - gap->service.adv_svc_uuid_len = 1; - adv_service_uid[0] = gap->config->adv_service_uuid & 0xff; - adv_service_uid[1] = gap->config->adv_service_uuid >> 8; - set_advertisment_service_uid(adv_service_uid, sizeof(adv_service_uid)); + if(gap->config->mfg_data_len > 0) { + // Offset by 2 for length + AD_TYPE_MANUFACTURER_SPECIFIC_DATA + gap->service.mfg_data_len = 2; + set_manufacturer_data(gap->config->mfg_data, gap->config->mfg_data_len); + } + + if(gap->config->adv_service.UUID_Type == UUID_TYPE_16) { + uint8_t adv_service_uid[2]; + gap->service.adv_svc_uuid_len = 1; + adv_service_uid[0] = gap->config->adv_service.Service_UUID_16 & 0xff; + adv_service_uid[1] = gap->config->adv_service.Service_UUID_16 >> 8; + set_advertisment_service_uid(adv_service_uid, sizeof(adv_service_uid)); + } else if(gap->config->adv_service.UUID_Type == UUID_TYPE_128) { + gap->service.adv_svc_uuid_len = 1; + set_advertisment_service_uid( + gap->config->adv_service.Service_UUID_128, + sizeof(gap->config->adv_service.Service_UUID_128)); + } else { + furi_crash("Invalid UUID type"); + } // Set callback gap->on_event_cb = on_event_cb; diff --git a/targets/f7/ble_glue/gap.h b/targets/f7/ble_glue/gap.h index a90d07304..d37fd9a1e 100644 --- a/targets/f7/ble_glue/gap.h +++ b/targets/f7/ble_glue/gap.h @@ -68,7 +68,13 @@ typedef struct { } GapConnectionParamsRequest; typedef struct { - uint16_t adv_service_uuid; + struct { + uint8_t UUID_Type; + uint16_t Service_UUID_16; + uint8_t Service_UUID_128[16]; + } adv_service; + uint8_t mfg_data[20]; + uint8_t mfg_data_len; uint16_t appearance_char; bool bonding_mode; GapPairing pairing_method; diff --git a/targets/f7/ble_glue/profiles/serial_profile.c b/targets/f7/ble_glue/profiles/serial_profile.c index 427539427..9bba0d897 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.c +++ b/targets/f7/ble_glue/profiles/serial_profile.c @@ -6,6 +6,7 @@ #include #include #include +#include typedef struct { FuriHalBleProfileBase base; @@ -47,7 +48,11 @@ static void ble_profile_serial_stop(FuriHalBleProfileBase* profile) { #define CONNECTION_INTERVAL_MAX (0x24) static const GapConfig serial_template_config = { - .adv_service_uuid = 0x3080, + .adv_service = + { + .UUID_Type = UUID_TYPE_16, + .Service_UUID_16 = 0x3080, + }, .appearance_char = 0x8600, .bonding_mode = true, .pairing_method = GapPairingPinCodeShow, @@ -71,7 +76,8 @@ static void config->adv_name, furi_hal_version_get_ble_local_device_name_ptr(), FURI_HAL_VERSION_DEVICE_NAME_LENGTH); - config->adv_service_uuid |= furi_hal_version_get_hw_color(); + config->adv_service.UUID_Type = UUID_TYPE_16; + config->adv_service.Service_UUID_16 |= furi_hal_version_get_hw_color(); } static const FuriHalBleProfileTemplate profile_callbacks = {