1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-12 20:49:49 +04:00

Merge remote-tracking branch 'OFW/dev' into dev

This commit is contained in:
MX
2025-04-01 22:55:16 +03:00
12 changed files with 912 additions and 34 deletions

View File

@@ -6,9 +6,12 @@
#include <storage/storage.h>
#include <applications/system/js_app/js_thread.h>
#include <applications/system/js_app/js_value.h>
#include <stdint.h>
#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);

View File

@@ -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; \

View File

@@ -8,6 +8,7 @@
#include <rpc/rpc_i.h>
#include <flipper.pb.h>
#include <applications/system/js_app/js_thread.h>
#include <applications/system/js_app/js_value.h>
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
@@ -38,4 +39,16 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
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)));

View File

@@ -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",

View File

@@ -2,6 +2,7 @@
#include <stdint.h>
#include "js_thread_i.h"
#include "js_value.h"
#include <flipper_application/flipper_application.h>
#include <flipper_application/plugins/plugin_manager.h>
#include <flipper_application/plugins/composite_resolver.h>

View File

@@ -0,0 +1,291 @@
#include "js_value.h"
#include <stdarg.h>
#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;
}

View File

@@ -0,0 +1,212 @@
#pragma once
#include <furi.h>
#include "js_modules.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
// literal types
JsValueTypeAny, //<! Literal term
JsValueTypeAnyArray, //<! Literal term, after ensuring that it's an array
JsValueTypeAnyObject, //<! Literal term, after ensuring that it's an object
JsValueTypeFunction, //<! Literal term, after ensuring that it's a function
// primitive types
JsValueTypeRawPointer, //<! Unchecked `void*`
JsValueTypeInt32, //<! Number cast to `int32_t`
JsValueTypeDouble, //<! Number cast to `double`
JsValueTypeString, //<! Any string cast to `const char*`
JsValueTypeBool, //<! Bool cast to `bool`
// types with children
JsValueTypeEnum, //<! String with predefined possible values cast to a C enum via a mapping
JsValueTypeObject, //<! Object with predefined recursive fields cast to several C values
JsValueTypeMask = 0xff,
// enum sizes
JsValueTypeEnumSize1 = (1 << 8),
JsValueTypeEnumSize2 = (2 << 8),
JsValueTypeEnumSize4 = (4 << 8),
// flags
JsValueTypePermitNull = (1 << 16), //<! If the value is absent, assign default value
} JsValueType;
#define JS_VALUE_TYPE_ENUM_SIZE(x) ((x) << 8)
typedef struct {
const char* string_value;
size_t num_value;
} JsValueEnumVariant;
typedef union {
void* ptr_val;
int32_t int32_val;
double double_val;
const char* str_val;
size_t enum_val;
bool bool_val;
} JsValueDefaultValue;
typedef struct JsValueObjectField JsValueObjectField;
typedef struct {
JsValueType type;
JsValueDefaultValue default_value;
size_t n_children;
union {
const JsValueEnumVariant* enum_variants;
const JsValueObjectField* object_fields;
};
} JsValueDeclaration;
struct JsValueObjectField {
const char* field_name;
const JsValueDeclaration* value;
};
typedef struct {
size_t n_children;
const JsValueDeclaration* arguments;
} JsValueArguments;
#define JS_VALUE_ENUM(c_type, variants) \
{ \
.type = JsValueTypeEnum | JS_VALUE_TYPE_ENUM_SIZE(sizeof(c_type)), \
.n_children = COUNT_OF(variants), \
.enum_variants = variants, \
}
#define JS_VALUE_ENUM_W_DEFAULT(c_type, variants, default) \
{ \
.type = JsValueTypeEnum | JsValueTypePermitNull | \
JS_VALUE_TYPE_ENUM_SIZE(sizeof(c_type)), \
.default_value.enum_val = default, \
.n_children = COUNT_OF(variants), \
.enum_variants = variants, \
}
#define JS_VALUE_OBJECT(fields) \
{ \
.type = JsValueTypeObject, \
.n_children = COUNT_OF(fields), \
.object_fields = fields, \
}
#define JS_VALUE_OBJECT_W_DEFAULTS(fields) \
{ \
.type = JsValueTypeObject | JsValueTypePermitNull, \
.n_children = COUNT_OF(fields), \
.object_fields = fields, \
}
#define JS_VALUE_SIMPLE(t) {.type = t}
#define JS_VALUE_SIMPLE_W_DEFAULT(t, name, val) \
{.type = (t) | JsValueTypePermitNull, .default_value.name = (val)}
#define JS_VALUE_ARGS(args) \
{ \
.n_children = COUNT_OF(args), \
.arguments = args, \
}
typedef enum {
JsValueParseFlagNone = 0,
JsValueParseFlagReturnOnError =
(1
<< 0), //<! Sets mjs error string to a description of the parsing error and returns from the JS function
} JsValueParseFlag;
typedef enum {
JsValueParseStatusOk, //<! Parsing completed successfully
JsValueParseStatusJsError, //<! Parsing failed due to incorrect JS input
} JsValueParseStatus;
typedef enum {
JsValueParseSourceValue,
JsValueParseSourceArguments,
} JsValueParseSource;
typedef struct {
JsValueParseSource source;
union {
const JsValueDeclaration* value_decl;
const JsValueArguments* argument_decl;
};
} JsValueParseDeclaration;
#define JS_VALUE_PARSE_SOURCE_VALUE(declaration) \
((JsValueParseDeclaration){.source = JsValueParseSourceValue, .value_decl = declaration})
#define JS_VALUE_PARSE_SOURCE_ARGS(declaration) \
((JsValueParseDeclaration){ \
.source = JsValueParseSourceArguments, .argument_decl = declaration})
/**
* @brief Determines the size of the buffer array of `mjs_val_t`s that needs to
* be passed to `js_value_parse`.
*/
size_t js_value_buffer_size(const JsValueParseDeclaration declaration);
/**
* @brief Converts a JS value into a series of C values.
*
* @param[in] mjs mJS instance pointer
* @param[in] declaration Declaration for the input value. Chooses where the
* values are to be fetched from (an `mjs_val_t` or
* function arguments)
* @param[in] flags See the corresponding enum.
* @param[out] buffer Temporary buffer for values that need to live
* longer than the function call. To determine the
* size of the buffer, use `js_value_buffer_size`.
* Values parsed by this function will become invalid
* when this buffer goes out of scope.
* @param[in] buf_size Number of entries in the temporary buffer (i.e.
* `COUNT_OF`, not `sizeof`).
* @param[in] source Source JS value that needs to be converted. May be
* NULL if `declaration.source` is
* `JsValueParseSourceArguments`.
* @param[in] n_c_vals Number of output C values
* @param[out] ... Pointers to output C values. The order in which
* these values are populated corresponds to the order
* in which the values are defined in the declaration.
*
* @returns Parsing status
*/
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,
...);
#define JS_VALUE_PARSE(mjs, declaration, flags, status_ptr, value_ptr, ...) \
void* _args[] = {__VA_ARGS__}; \
size_t _n_args = COUNT_OF(_args); \
size_t _temp_buf_len = js_value_buffer_size(declaration); \
mjs_val_t _temp_buffer[_temp_buf_len]; \
*(status_ptr) = js_value_parse( \
mjs, declaration, flags, _temp_buffer, _temp_buf_len, value_ptr, _n_args, __VA_ARGS__);
#define JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, declaration, ...) \
JsValueParseStatus _status; \
JS_VALUE_PARSE( \
mjs, \
JS_VALUE_PARSE_SOURCE_ARGS(declaration), \
JsValueParseFlagReturnOnError, \
&_status, \
NULL, \
__VA_ARGS__); \
if(_status != JsValueParseStatusOk) return;
#ifdef __cplusplus
}
#endif

View File

@@ -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:

View File

@@ -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,

View File

@@ -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;
@@ -216,11 +218,12 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) {
gap->service.connection_handle = event->Connection_Handle;
gap_verify_connection_parameters(gap);
// Save rssi for current connection
fetch_rssi();
// 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:
@@ -345,6 +348,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];
@@ -464,6 +475,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,
@@ -577,11 +593,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;

View File

@@ -69,7 +69,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;

View File

@@ -6,6 +6,7 @@
#include <services/battery_service.h>
#include <services/serial_service.h>
#include <furi.h>
#include <ble/core/ble_defs.h>
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 = {