Compare commits

..

3 Commits

Author SHA1 Message Date
Andrea Santaniello
a37ba6b815 Updated targets 2026-03-13 19:43:34 +01:00
Andrea Santaniello
ab1231667c Eradicated JS interpreter & related stuff 2026-03-13 19:07:14 +01:00
Andrea Santaniello
fd7d8c1ea8 Added CRC support to the protocol table 2026-03-13 17:18:39 +01:00
232 changed files with 61 additions and 33011 deletions

110
README.md
View File

@@ -42,67 +42,67 @@ This project may incorporate, adapt, or build upon **other open-source projects*
### Automotive Protocols
| Manufacturer | Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---|:---:|:---:|:---:|:---:|
| VAG (VW/Audi/Skoda/Seat) | VAG GROUP | 433 MHz | AM | Yes | Yes |
| Porsche | Cayenne | 433/868 MHz | AM | Yes | Yes |
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes |
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes |
| Fiat | Fiat Marelli | 433 MHz | AM | No | Yes |
| Subaru | Subaru | 433 MHz | AM | Yes | Yes |
| Mazda | Siemens (5WK49365D) | 315/433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V0 | 433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V1 | 315/433 MHz | AM | Yes | Yes |
| Kia/Hyundai | Kia V2 | 315/433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V3/V4 | 315/433 MHz | AM/FM | Yes | Yes |
| Kia/Hyundai | Kia V5 | 433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V6 | 433 MHz | FM | Yes | Yes |
| Suzuki | Suzuki | 433 MHz | FM | Yes | Yes |
| Mitsubishi | Mitsubishi V0 | 868 MHz | FM | Yes | Yes |
| Manufacturer | Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---|:---:|:---:|:---:|:---:|:---:|
| VAG (VW/Audi/Skoda/Seat) | VAG GROUP | 433 MHz | AM | Yes | Yes | No |
| Porsche | Cayenne | 433/868 MHz | AM | Yes | Yes | No |
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes | Yes |
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes | Yes |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes | Yes |
| Fiat | Fiat Marelli | 433 MHz | AM | No | Yes | No |
| Subaru | Subaru | 433 MHz | AM | Yes | Yes | No |
| Mazda | Siemens (5WK49365D) | 315/433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V0 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V1 | 315/433 MHz | AM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V2 | 315/433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V3/V4 | 315/433 MHz | AM/FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V5 | 433 MHz | FM | Yes | Yes | Yes |
| Kia/Hyundai | Kia V6 | 433 MHz | FM | Yes | Yes | Yes |
| Suzuki | Suzuki | 433 MHz | FM | Yes | Yes | Yes |
| Mitsubishi | Mitsubishi V0 | 868 MHz | FM | Yes | Yes | No |
### Gate / Access Protocols
| Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---:|:---:|:---:|:---:|
| Keeloq | 433/868/315 MHz | AM | Yes | Yes |
| Nice FLO | 433 MHz | AM | Yes | Yes |
| Nice FloR-S | 433 MHz | AM | Yes | Yes |
| CAME | 433/315 MHz | AM | Yes | Yes |
| CAME TWEE | 433 MHz | AM | Yes | Yes |
| CAME Atomo | 433 MHz | AM | Yes | Yes |
| Faac SLH | 433/868 MHz | AM | Yes | Yes |
| Somfy Telis | 433 MHz | AM | Yes | Yes |
| Somfy Keytis | 433 MHz | AM | Yes | Yes |
| Alutech AT-4N | 433 MHz | AM | Yes | Yes |
| KingGates Stylo4k | 433 MHz | AM | Yes | Yes |
| Beninca ARC | 433 MHz | AM | Yes | Yes |
| Hormann HSM | 433/868 MHz | AM | Yes | Yes |
| Marantec | 433 MHz | AM | Yes | Yes |
| Marantec24 | 433 MHz | AM | Yes | Yes |
| Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---:|:---:|:---:|:---:|:---:|
| Keeloq | 433/868/315 MHz | AM | Yes | Yes | No |
| Nice FLO | 433 MHz | AM | Yes | Yes | No |
| Nice FloR-S | 433 MHz | AM | Yes | Yes | Yes |
| CAME | 433/315 MHz | AM | Yes | Yes | No |
| CAME TWEE | 433 MHz | AM | Yes | Yes | No |
| CAME Atomo | 433 MHz | AM | Yes | Yes | No |
| Faac SLH | 433/868 MHz | AM | Yes | Yes | No |
| Somfy Telis | 433 MHz | AM | Yes | Yes | Yes |
| Somfy Keytis | 433 MHz | AM | Yes | Yes | Yes |
| Alutech AT-4N | 433 MHz | AM | Yes | Yes | Yes |
| KingGates Stylo4k | 433 MHz | AM | Yes | Yes | No |
| Beninca ARC | 433 MHz | AM | Yes | Yes | No |
| Hormann HSM | 433/868 MHz | AM | Yes | Yes | No |
| Marantec | 433 MHz | AM | Yes | Yes | Yes |
| Marantec24 | 433 MHz | AM | Yes | Yes | Yes |
### General Static Protocols
| Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---:|:---:|:---:|:---:|
| Princeton | 433/315 MHz | AM | Yes | Yes |
| Linear | 315 MHz | AM | Yes | Yes |
| LinearDelta3 | 315 MHz | AM | Yes | Yes |
| GateTX | 433 MHz | AM | Yes | Yes |
| Security+ 1.0 | 315 MHz | AM | Yes | Yes |
| Security+ 2.0 | 315 MHz | AM | Yes | Yes |
| Chamberlain Code | 315 MHz | AM | Yes | Yes |
| MegaCode | 315 MHz | AM | Yes | Yes |
| Mastercode | 433 MHz | AM | Yes | Yes |
| Dickert MAHS | 433 MHz | AM | Yes | Yes |
| SMC5326 | 433 MHz | AM | Yes | Yes |
| Phoenix V2 | 433 MHz | AM | Yes | Yes |
| Doitrand | 433 MHz | AM | Yes | Yes |
| Hay21 | 433 MHz | AM | Yes | Yes |
| Revers RB2 | 433 MHz | AM | Yes | Yes |
| Roger | 433 MHz | AM | Yes | Yes |
| BinRAW | 433/315/868 MHz | AM/FM | Yes | Yes |
| RAW | All | All | Yes | Yes |
| Protocol | Frequency | Modulation | Encoder | Decoder | CRC |
|:---|:---:|:---:|:---:|:---:|:---:|
| Princeton | 433/315 MHz | AM | Yes | Yes | No |
| Linear | 315 MHz | AM | Yes | Yes | No |
| LinearDelta3 | 315 MHz | AM | Yes | Yes | No |
| GateTX | 433 MHz | AM | Yes | Yes | No |
| Security+ 1.0 | 315 MHz | AM | Yes | Yes | No |
| Security+ 2.0 | 315 MHz | AM | Yes | Yes | No |
| Chamberlain Code | 315 MHz | AM | Yes | Yes | No |
| MegaCode | 315 MHz | AM | Yes | Yes | No |
| Mastercode | 433 MHz | AM | Yes | Yes | No |
| Dickert MAHS | 433 MHz | AM | Yes | Yes | No |
| SMC5326 | 433 MHz | AM | Yes | Yes | No |
| Phoenix V2 | 433 MHz | AM | Yes | Yes | No |
| Doitrand | 433 MHz | AM | Yes | Yes | No |
| Hay21 | 433 MHz | AM | Yes | Yes | No |
| Revers RB2 | 433 MHz | AM | Yes | Yes | No |
| Roger | 433 MHz | AM | Yes | Yes | No |
| BinRAW | 433/315/868 MHz | AM/FM | Yes | Yes | No |
| RAW | All | All | Yes | Yes | No |
---

View File

@@ -222,13 +222,6 @@ App(
requires=["unit_tests"],
)
App(
appid="test_js",
sources=["tests/common/*.c", "tests/js/*.c"],
apptype=FlipperAppType.PLUGIN,
entry_point="get_api",
requires=["unit_tests", "js_app"],
)
App(
appid="test_strint",

View File

@@ -1,395 +0,0 @@
#include "../test.h" // IWYU pragma: keep
#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_random.h>
#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 {
JsTestsFinished = 1,
JsTestsError = 2,
} JsTestFlag;
typedef struct {
FuriEventFlag* event_flags;
FuriString* error_string;
} JsTestCallbackContext;
static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
JsTestCallbackContext* context = param;
if(event == JsThreadEventPrint) {
FURI_LOG_I("js_test", "%s", msg);
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
context->error_string = furi_string_alloc_set_str(msg);
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
} else if(event == JsThreadEventDone) {
furi_event_flag_set(context->event_flags, JsTestsFinished);
}
}
static void js_test_run(const char* script_path) {
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
context->event_flags = furi_event_flag_alloc();
JsThread* thread = js_thread_run(script_path, js_test_callback, context);
uint32_t flags = furi_event_flag_wait(
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
if(flags & FuriFlagError) {
// getting the flags themselves should not fail
furi_crash();
}
FuriString* error_string = context->error_string;
js_thread_stop(thread);
furi_event_flag_free(context->event_flags);
free(context);
if(flags & JsTestsError) {
// memory leak: not freeing the FuriString if the tests fail,
// because mu_fail executes a return
//
// who cares tho?
mu_fail(furi_string_get_cstr(error_string));
}
}
MU_TEST(js_test_basic) {
js_test_run(JS_SCRIPT_PATH("basic"));
}
MU_TEST(js_test_math) {
js_test_run(JS_SCRIPT_PATH("math"));
}
MU_TEST(js_test_event_loop) {
js_test_run(JS_SCRIPT_PATH("event_loop"));
}
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);
MU_RUN_TEST(js_test_storage);
}
int run_minunit_test_js(void) {
MU_RUN_SUITE(test_js);
return MU_EXIT_CODE;
}
TEST_API_DEFINE(run_minunit_test_js)

View File

@@ -7,8 +7,6 @@
#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*)),
@@ -34,21 +32,4 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
xQueueGenericSend,
BaseType_t,
(QueueHandle_t, const void* const, TickType_t, const BaseType_t)),
API_METHOD(
js_thread_run,
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

@@ -30,11 +30,9 @@ static const char* const known_ext[] = {
[ArchiveFileTypeBadUsb] = ".txt",
[ArchiveFileTypeU2f] = "?",
[ArchiveFileTypeApplication] = ".fap",
[ArchiveFileTypeJS] = ".js",
[ArchiveFileTypeUpdateManifest] = ".fuf",
[ArchiveFileTypeFolder] = "?",
[ArchiveFileTypeUnknown] = "*",
[ArchiveFileTypeAppOrJs] = ".fap|.js",
[ArchiveFileTypeSetting] = "?",
};
@@ -47,7 +45,7 @@ static const ArchiveFileTypeEnum known_type[] = {
[ArchiveTabInfrared] = ArchiveFileTypeInfrared,
[ArchiveTabBadUsb] = ArchiveFileTypeBadUsb,
[ArchiveTabU2f] = ArchiveFileTypeU2f,
[ArchiveTabApplications] = ArchiveFileTypeAppOrJs,
[ArchiveTabApplications] = ArchiveFileTypeApplication,
[ArchiveTabInternal] = ArchiveFileTypeUnknown,
[ArchiveTabBrowser] = ArchiveFileTypeUnknown,
};

View File

@@ -18,10 +18,8 @@ typedef enum {
ArchiveFileTypeU2f,
ArchiveFileTypeApplication,
ArchiveFileTypeUpdateManifest,
ArchiveFileTypeJS,
ArchiveFileTypeFolder,
ArchiveFileTypeUnknown,
ArchiveFileTypeAppOrJs,
ArchiveFileTypeSetting,
ArchiveFileTypeLoading,
} ArchiveFileTypeEnum;

View File

@@ -29,8 +29,6 @@ static const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) {
return "U2F";
case ArchiveFileTypeUpdateManifest:
return "UpdaterApp";
case ArchiveFileTypeJS:
return "JS Runner";
case ArchiveFileTypeFolder:
return "Archive";
default:

View File

@@ -36,8 +36,6 @@ static const Icon* ArchiveItemIcons[] = {
[ArchiveFileTypeFolder] = &I_dir_10px,
[ArchiveFileTypeUnknown] = &I_unknown_10px,
[ArchiveFileTypeLoading] = &I_loading_10px,
[ArchiveFileTypeJS] = &I_js_script_10px,
[ArchiveFileTypeAppOrJs] = &I_unknown_10px,
};
void archive_browser_set_callback(

View File

@@ -11,8 +11,6 @@
#define TAG "LoaderApplications"
#define JS_RUNNER_APP "JS Runner"
struct LoaderApplications {
FuriThread* thread;
void (*closed_cb)(void*);
@@ -86,19 +84,13 @@ static bool loader_applications_item_callback(
FuriString* item_name) {
LoaderApplicationsApp* loader_applications_app = context;
furi_assert(loader_applications_app);
if(furi_string_end_with(path, ".fap")) {
return flipper_application_load_name_and_icon(
path, loader_applications_app->storage, icon_ptr, item_name);
} else {
path_extract_filename(path, item_name, false);
memcpy(*icon_ptr, icon_get_frame_data(&I_js_script_10px, 0), FAP_MANIFEST_MAX_ICON_SIZE);
return true;
}
return flipper_application_load_name_and_icon(
path, loader_applications_app->storage, icon_ptr, item_name);
}
static bool loader_applications_select_app(LoaderApplicationsApp* loader_applications_app) {
const DialogsFileBrowserOptions browser_options = {
.extension = ".fap|.js",
.extension = ".fap",
.skip_assets = true,
.icon = &I_unknown_10px,
.hide_ext = true,
@@ -152,12 +144,7 @@ static int32_t loader_applications_thread(void* p) {
view_holder_set_view(app->view_holder, loading_get_view(app->loading));
while(loader_applications_select_app(app)) {
if(!furi_string_end_with(app->file_path, ".js")) {
loader_applications_start_app(app, furi_string_get_cstr(app->file_path), NULL);
} else {
loader_applications_start_app(
app, JS_RUNNER_APP, furi_string_get_cstr(app->file_path));
}
loader_applications_start_app(app, furi_string_get_cstr(app->file_path), NULL);
}
// stop loading animation

View File

@@ -4,7 +4,6 @@ App(
apptype=FlipperAppType.METAPACKAGE,
provides=[
"updater_app",
"js_app",
# "archive",
],
)

View File

@@ -1,281 +0,0 @@
App(
appid="js_app",
name="JS Runner",
apptype=FlipperAppType.SYSTEM,
entry_point="js_app",
stack_size=2 * 1024,
resources="examples",
order=10,
provides=["js_app_start"],
sources=[
"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",
"modules/js_tests.c",
],
)
App(
appid="js_app_start",
apptype=FlipperAppType.STARTUP,
entry_point="js_app_on_system_start",
order=110,
sources=["js_app.c"],
)
App(
appid="js_event_loop",
apptype=FlipperAppType.PLUGIN,
entry_point="js_event_loop_ep",
requires=["js_app"],
sources=[
"modules/js_event_loop/js_event_loop.c",
"modules/js_event_loop/js_event_loop_api_table.cpp",
],
)
App(
appid="js_gui",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_ep",
requires=["js_app"],
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
)
App(
appid="js_gui__loading",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_loading_ep",
requires=["js_app"],
sources=["modules/js_gui/loading.c"],
)
App(
appid="js_gui__empty_screen",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_empty_screen_ep",
requires=["js_app"],
sources=["modules/js_gui/empty_screen.c"],
)
App(
appid="js_gui__submenu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_submenu_ep",
requires=["js_app"],
sources=["modules/js_gui/submenu.c"],
)
App(
appid="js_gui__text_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_text_input_ep",
requires=["js_app"],
sources=["modules/js_gui/text_input.c"],
)
App(
appid="js_gui__number_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_number_input_ep",
requires=["js_app"],
sources=["modules/js_gui/number_input.c"],
)
App(
appid="js_gui__button_panel",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_panel_ep",
requires=["js_app"],
sources=["modules/js_gui/button_panel.c"],
)
App(
appid="js_gui__popup",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_popup_ep",
requires=["js_app"],
sources=["modules/js_gui/popup.c"],
)
App(
appid="js_gui__button_menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/button_menu.c"],
)
App(
appid="js_gui__menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/menu.c"],
)
App(
appid="js_gui__vi_list",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_vi_list_ep",
requires=["js_app"],
sources=["modules/js_gui/vi_list.c"],
)
App(
appid="js_gui__byte_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_byte_input_ep",
requires=["js_app"],
sources=["modules/js_gui/byte_input.c"],
)
App(
appid="js_gui__text_box",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_text_box_ep",
requires=["js_app"],
sources=["modules/js_gui/text_box.c"],
)
App(
appid="js_gui__dialog",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_dialog_ep",
requires=["js_app"],
sources=["modules/js_gui/dialog.c"],
)
App(
appid="js_gui__file_picker",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_file_picker_ep",
requires=["js_app"],
sources=["modules/js_gui/file_picker.c"],
fap_libs=["assets"],
)
App(
appid="js_gui__widget",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_widget_ep",
requires=["js_app"],
sources=["modules/js_gui/widget.c"],
)
App(
appid="js_gui__icon",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_icon_ep",
requires=["js_app"],
sources=["modules/js_gui/icon.c"],
fap_libs=["assets"],
)
App(
appid="js_notification",
apptype=FlipperAppType.PLUGIN,
entry_point="js_notification_ep",
requires=["js_app"],
sources=["modules/js_notification.c"],
)
App(
appid="js_badusb",
apptype=FlipperAppType.PLUGIN,
entry_point="js_badusb_ep",
requires=["js_app"],
sources=["modules/js_badusb.c"],
)
App(
appid="js_serial",
apptype=FlipperAppType.PLUGIN,
entry_point="js_serial_ep",
requires=["js_app"],
sources=["modules/js_serial.c"],
)
App(
appid="js_gpio",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gpio_ep",
requires=["js_app"],
sources=["modules/js_gpio.c"],
)
App(
appid="js_math",
apptype=FlipperAppType.PLUGIN,
entry_point="js_math_ep",
requires=["js_app"],
sources=["modules/js_math.c"],
)
App(
appid="js_storage",
apptype=FlipperAppType.PLUGIN,
entry_point="js_storage_ep",
requires=["js_app"],
sources=["modules/js_storage.c"],
)
App(
appid="js_vgm",
apptype=FlipperAppType.PLUGIN,
entry_point="js_vgm_ep",
requires=["js_app"],
sources=["modules/js_vgm/*.c", "modules/js_vgm/ICM42688P/*.c"],
)
App(
appid="js_subghz",
apptype=FlipperAppType.PLUGIN,
entry_point="js_subghz_ep",
requires=["js_app"],
sources=["modules/js_subghz/*.c"],
)
App(
appid="js_infrared",
apptype=FlipperAppType.PLUGIN,
entry_point="js_infrared_ep",
requires=["js_app"],
sources=["modules/js_infrared/*.c"],
)
App(
appid="js_blebeacon",
apptype=FlipperAppType.PLUGIN,
entry_point="js_blebeacon_ep",
requires=["js_app"],
sources=["modules/js_blebeacon.c"],
)
App(
appid="js_usbdisk",
apptype=FlipperAppType.PLUGIN,
entry_point="js_usbdisk_ep",
requires=["js_app"],
sources=["modules/js_usbdisk/*.c"],
)
App(
appid="js_i2c",
apptype=FlipperAppType.PLUGIN,
entry_point="js_i2c_ep",
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"],
)

View File

@@ -1,8 +0,0 @@
let arr_1 = Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
print("len =", arr_1.buffer.byteLength);
let arr_2 = Uint8Array(arr_1.buffer.slice(2, 6));
print("slice len =", arr_2.buffer.byteLength);
for (let i = 0; i < arr_2.buffer.byteLength; i++) {
print(arr_2[i]);
}

View File

@@ -1,20 +0,0 @@
let serial = require("serial");
serial.setup("lpuart", 115200);
// serial.write("\n");
serial.write([0x0a]);
let console_resp = serial.expect("# ", 1000);
if (console_resp === undefined) {
print("No CLI response");
} else {
serial.write("uci\n");
let uci_state = serial.expect([": not found", "Usage: "]);
if (uci_state === 1) {
serial.expect("# ");
serial.write("uci show wireless\n");
serial.expect(".key=");
print("key:", serial.readln());
} else {
print("uci cmd not found");
}
}

View File

@@ -1,73 +0,0 @@
let badusb = require("badusb");
let notify = require("notification");
let flipper = require("flipper");
let eventLoop = require("event_loop");
let gui = require("gui");
let dialog = require("gui/dialog");
let views = {
dialog: dialog.makeWith({
header: "BadUSB demo",
text: "Press OK to start",
center: "Start",
}),
};
badusb.setup({
vid: 0xAAAA,
pid: 0xBBBB,
mfrName: "Flipper",
prodName: "Zero",
layoutPath: "/ext/badusb/assets/layouts/en-US.kl"
});
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
if (button !== "center")
return;
gui.viewDispatcher.sendTo("back");
if (badusb.isConnected()) {
notify.blink("green", "short");
print("USB is connected");
badusb.println("Hello, world!");
badusb.press("CTRL", "a");
badusb.press("CTRL", "c");
badusb.press("DOWN");
delay(1000);
badusb.press("CTRL", "v");
delay(1000);
badusb.press("CTRL", "v");
badusb.println("1234", 200);
badusb.println("Flipper Model: " + flipper.getModel());
badusb.println("Flipper Name: " + flipper.getName());
badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%");
// Alt+Numpad method works only on Windows!!!
badusb.altPrintln("This was printed with Alt+Numpad method!");
// There's also badusb.print() and badusb.altPrint()
// which don't add the return at the end
notify.success();
} else {
print("USB not connected");
notify.error();
}
// Optional, but allows to unlock usb interface to switch profile
badusb.quit();
eventLoop.stop();
}, eventLoop, gui);
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _item, eventLoop) {
eventLoop.stop();
}, eventLoop);
gui.viewDispatcher.switchTo(views.dialog);
eventLoop.run();

View File

@@ -1,62 +0,0 @@
// Script cannot work without blebeacon module so check before
checkSdkFeatures(["blebeacon"]);
let blebeacon = require("blebeacon");
// Stop if previous background beacon is active
if (blebeacon.isActive()) {
blebeacon.stop();
}
// Make sure it resets at script exit, true will keep advertising in background
// This is false by default, can be omitted
blebeacon.keepAlive(false);
let math = require("math");
let currentIndex = 0;
let watchValues = [
0x1A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x11, 0x12, 0x13, 0x14, 0x15,
0x16, 0x17, 0x18, 0xE4, 0xE5, 0x1B, 0x1C, 0x1D, 0x1E,
0x20, 0xEC, 0xEF
];
function generateRandomMac() {
let mac = [];
for (let i = 0; i < 6; i++) {
mac.push(math.floor(math.random() * 256));
}
return Uint8Array(mac);
}
function sendRandomModelAdvertisement() {
let model = watchValues[currentIndex];
let packet = [
14, 0xFF, 0x75, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x43,
model
];
let intervalMs = 50;
// Power level, min interval and max interval are optional
blebeacon.setConfig(generateRandomMac(), 0x1F, intervalMs, intervalMs * 3);
blebeacon.setData(Uint8Array(packet));
blebeacon.start();
print("Sent data for model ID " + model.toString());
currentIndex = (currentIndex + 1) % watchValues.length;
delay(intervalMs);
blebeacon.stop();
}
while (true) {
sendRandomModelAdvertisement();
}

View File

@@ -1,5 +0,0 @@
print("print", 1);
console.log("log", 2);
console.warn("warn", 3);
console.error("error", 4);
console.debug("debug", 5);

View File

@@ -1,9 +0,0 @@
print("start");
delay(1000)
print("1");
delay(1000)
print("2");
delay(1000)
print("3");
delay(1000)
print("end");

View File

@@ -1,25 +0,0 @@
let eventLoop = require("event_loop");
// print a string after 1337 milliseconds
eventLoop.subscribe(eventLoop.timer("oneshot", 1337), function (_subscription, _item) {
print("Hi after 1337 ms");
});
// count up to 5 with a delay of 100ms between increments
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, counter) {
print("Counter two:", counter);
if (counter === 5)
subscription.cancel();
return [counter + 1];
}, 0);
// count up to 15 with a delay of 100ms between increments
// and stop the program when the count reaches 15
eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, event_loop, counter) {
print("Counter one:", counter);
if (counter === 15)
event_loop.stop();
return [event_loop, counter + 1];
}, eventLoop, 0);
eventLoop.run();

View File

@@ -1,65 +0,0 @@
let eventLoop = require("event_loop");
let gpio = require("gpio");
// initialize pins
let led = gpio.get("pc3"); // same as `gpio.get(7)`
let led2 = gpio.get("pa7"); // same as `gpio.get(2)`
let pot = gpio.get("pc0"); // same as `gpio.get(16)`
let button = gpio.get("pc1"); // same as `gpio.get(15)`
led.init({ direction: "out", outMode: "push_pull" });
pot.init({ direction: "in", inMode: "analog" });
button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
// blink led
print("Commencing blinking (PC3)");
eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, state) {
led.write(state);
return [led, !state];
}, led, true);
// cycle led pwm
print("Commencing PWM (PA7)");
eventLoop.subscribe(eventLoop.timer("periodic", 10), function (_, _item, led2, state) {
led2.pwmWrite(10000, state);
return [led2, (state + 1) % 101];
}, led2, 0);
// read potentiometer when button is pressed
print("Press the button (PC1)");
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
print("PC0 is at", pot.readAnalog(), "mV");
}, pot);
// the program will just exit unless this is here
eventLoop.run();
// possible pins https://docs.flipper.net/gpio-and-modules#miFsS
// "PA7" aka 2
// "PA6" aka 3
// "PA4" aka 4
// "PB3" aka 5
// "PB2" aka 6
// "PC3" aka 7
// "PA14" aka 10
// "PA13" aka 12
// "PB6" aka 13
// "PB7" aka 14
// "PC1" aka 15
// "PC0" aka 16
// "PB14" aka 17
// possible modes
// { direction: "out", outMode: "push_pull" }
// { direction: "out", outMode: "open_drain" }
// { direction: "out", outMode: "push_pull", altFn: true }
// { direction: "out", outMode: "open_drain", altFn: true }
// { direction: "in", inMode: "analog" }
// { direction: "in", inMode: "plain_digital" }
// { direction: "in", inMode: "interrupt", edge: "rising" }
// { direction: "in", inMode: "interrupt", edge: "falling" }
// { direction: "in", inMode: "interrupt", edge: "both" }
// { direction: "in", inMode: "event", edge: "rising" }
// { direction: "in", inMode: "event", edge: "falling" }
// { direction: "in", inMode: "event", edge: "both" }
// all variants support an optional `pull` field which can either be undefined,
// "up" or "down"

View File

@@ -1,265 +0,0 @@
// import modules
let eventLoop = require("event_loop");
let gui = require("gui");
let loadingView = require("gui/loading");
let submenuView = require("gui/submenu");
let emptyView = require("gui/empty_screen");
let textInputView = require("gui/text_input");
let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker");
let buttonMenuView = require("gui/button_menu");
let buttonPanelView = require("gui/button_panel");
let menuView = require("gui/menu");
let numberInputView = require("gui/number_input");
let popupView = require("gui/popup");
let viListView = require("gui/vi_list");
let widget = require("gui/widget");
let icon = require("gui/icon");
let flipper = require("flipper");
let math = require("math");
// declare clock widget children
let cuteDolphinWithWatch = icon.getBuiltin("DolphinWait_59x54");
let jsLogo = icon.getBuiltin("js_script_10px");
let stopwatchWidgetElements = [
{ element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" },
{ element: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" },
{ element: "rect", x: 64, y: 27, w: 28, h: 20, radius: 3, fill: false },
{ element: "rect", x: 100, y: 27, w: 28, h: 20, radius: 3, fill: false },
{ element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch },
{ element: "icon", x: 64, y: 13, iconData: jsLogo },
{ element: "button", button: "right", text: "Back" },
];
// icons for the button panel
let offIcons = [icon.getBuiltin("off_19x20"), icon.getBuiltin("off_hover_19x20")];
let powerIcons = [icon.getBuiltin("power_19x20"), icon.getBuiltin("power_hover_19x20")];
let settingsIcon = icon.getBuiltin("Settings_14");
// declare view instances
let views = {
loading: loadingView.make(),
empty: emptyView.make(),
keyboard: textInputView.makeWith({
header: "Enter your name",
minLength: 0,
maxLength: 32,
defaultText: flipper.getName(),
defaultTextClear: true,
}),
helloDialog: dialogView.make(),
bytekb: byteInputView.makeWith({
header: "Look ma, I'm a header text!",
length: 8,
defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]),
}),
longText: textBoxView.makeWith({
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
}),
stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements),
buttonMenu: buttonMenuView.makeWith({
header: "Header"
}, [
{ type: "common", label: "Test" },
{ type: "control", label: "Test2" },
]),
buttonPanel: buttonPanelView.makeWith({
matrixSizeX: 2,
matrixSizeY: 2,
}, [
{ type: "button", x: 0, y: 0, matrixX: 0, matrixY: 0, icon: offIcons[0], iconSelected: offIcons[1] },
{ type: "button", x: 30, y: 30, matrixX: 1, matrixY: 1, icon: powerIcons[0], iconSelected: powerIcons[1] },
{ type: "label", x: 0, y: 50, text: "Label", font: "primary" },
]),
menu: menuView.makeWith({}, [
{ label: "One", icon: settingsIcon },
{ label: "Two", icon: settingsIcon },
{ label: "three", icon: settingsIcon },
]),
numberKbd: numberInputView.makeWith({
header: "Number input",
defaultValue: 100,
minValue: 0,
maxValue: 200,
}),
popup: popupView.makeWith({
header: "Hello",
text: "I'm going to be gone\nin 2 seconds",
}),
viList: viListView.makeWith({}, [
{ label: "One", variants: ["1", "1.0"] },
{ label: "Two", variants: ["2", "2.0"] },
]),
demos: submenuView.makeWith({
header: "Choose a demo",
}, [
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Widget",
"Button menu",
"Button panel",
"Menu",
"Number input",
"Popup",
"Var. item list",
"Exit app",
]),
};
// Enable illegal filename symbols since we're not choosing filenames, gives more flexibility
// Not available in all firmwares, good idea to check if it is supported
if (doesSdkSupport(["gui-textinput-illegalsymbols"])) {
views.keyboard.set("illegalSymbols", true);
}
// demo selector
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
if (index === 0) {
gui.viewDispatcher.switchTo(views.loading);
// the loading view captures all back events, preventing our navigation callback from firing
// switch to the demo chooser after a second
eventLoop.subscribe(eventLoop.timer("oneshot", 1000), function (_sub, _, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
} else if (index === 1) {
gui.viewDispatcher.switchTo(views.empty);
} else if (index === 2) {
gui.viewDispatcher.switchTo(views.keyboard);
} else if (index === 3) {
gui.viewDispatcher.switchTo(views.bytekb);
} else if (index === 4) {
gui.viewDispatcher.switchTo(views.longText);
} else if (index === 5) {
let path = filePicker.pickFile("/ext", "*");
if (path) {
views.helloDialog.set("text", "You selected:\n" + path);
} else {
views.helloDialog.set("text", "You didn't select a file");
}
views.helloDialog.set("center", "Nice!");
gui.viewDispatcher.switchTo(views.helloDialog);
} else if (index === 6) {
gui.viewDispatcher.switchTo(views.stopwatchWidget);
} else if (index === 7) {
gui.viewDispatcher.switchTo(views.buttonMenu);
} else if (index === 8) {
gui.viewDispatcher.switchTo(views.buttonPanel);
} else if (index === 9) {
gui.viewDispatcher.switchTo(views.menu);
} else if (index === 10) {
gui.viewDispatcher.switchTo(views.numberKbd);
} else if (index === 11) {
views.popup.set("timeout", 2000);
gui.viewDispatcher.switchTo(views.popup);
} else if (index === 12) {
gui.viewDispatcher.switchTo(views.viList);
} else if (index === 13) {
eventLoop.stop();
}
}, gui, eventLoop, views);
// say hi after keyboard input
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
views.keyboard.set("defaultText", name); // Remember for next usage
views.helloDialog.set("text", "Hi " + name + "! :)");
views.helloDialog.set("center", "Hi Flipper! :)");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// go back after the greeting dialog
eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) {
if (button === "center")
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// show data after byte input
eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
let data_view = Uint8Array(data);
let text = "0x";
for (let i = 0; i < data_view.length; i++) {
text += data_view[i].toString(16);
}
views.helloDialog.set("text", "You typed:\n" + text);
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// go to the demo chooser screen when the back key is pressed
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views, eventLoop) {
if (gui.viewDispatcher.currentView === views.demos) {
eventLoop.stop();
return;
}
gui.viewDispatcher.switchTo(views.demos);
}, gui, views, eventLoop);
// go to the demo chooser screen when the right key is pressed on the widget screen
eventLoop.subscribe(views.stopwatchWidget.button, function (_sub, buttonEvent, gui, views) {
if (buttonEvent.key === "right" && buttonEvent.type === "short")
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// count time
eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, views, stopwatchWidgetElements, halfSeconds) {
let text = math.floor(halfSeconds / 2 / 60).toString();
if (halfSeconds < 10 * 60 * 2)
text = "0" + text;
text += (halfSeconds % 2 === 0) ? ":" : " ";
if (((halfSeconds / 2) % 60) < 10)
text += "0";
text += (math.floor(halfSeconds / 2) % 60).toString();
stopwatchWidgetElements[0].text = text;
views.stopwatchWidget.setChildren(stopwatchWidgetElements);
halfSeconds++;
return [views, stopwatchWidgetElements, halfSeconds];
}, views, stopwatchWidgetElements, 0);
// go back after popup times out
eventLoop.subscribe(views.popup.timeout, function (_sub, _item, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// button menu callback
eventLoop.subscribe(views.buttonMenu.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// button panel callback
eventLoop.subscribe(views.buttonPanel.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.menu.chosen, function (_sub, index, gui, views) {
views.helloDialog.set("text", "You selected #" + index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.numberKbd.input, function (_sub, number, gui, views) {
views.helloDialog.set("text", "You typed " + number.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// ignore VI list
eventLoop.subscribe(views.viList.valueUpdate, function (_sub, _item) {});
// run UI
gui.viewDispatcher.switchTo(views.demos);
eventLoop.run();

View File

@@ -1,51 +0,0 @@
// Script cannot work without i2c module so check before
checkSdkFeatures(["i2c"]);
// Connect an 24C32N EEPROM to the I2C bus of the board. SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8.
let i2c = require("i2c");
function i2c_find_first_device() {
let addr = -1;
for (let try_addr = 0; try_addr !== 0xff; try_addr++) {
if (i2c.isDeviceReady(try_addr, 5)) {
addr = try_addr;
break;
}
}
return addr;
}
let addr = i2c_find_first_device();
if (addr === -1) {
print("I2C device not found");
print("Please connect a 24C32N EEPROM I2C device to the Flipper Zero.");
print("SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8.");
} else {
print("I2C device found at address: " + addr.toString(16));
delay(1000);
// first two bytes are the start address (0x0000)
// the remaining bytes are the data to store.
// can also use Uint8Array([0x00, 0x00, ...]) as write parameter
i2c.write(addr, [0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47]);
while (i2c.isDeviceReady(addr, 9) === false) {
print("Waiting for device to be ready...");
}
// write the address to read from (we start at address 0x0001)
// read 3 bytes - 0x42, 0x43, 0x44
let data_buf = i2c.writeRead(addr, [0x00, 0x01], 3, 100);
let data = Uint8Array(data_buf);
print("Read bytes: " + data.length.toString());
for (let i = 0; i < data.length; i++) {
print("data[" + i.toString() + "] = " + data[i].toString(16));
}
// read two more bytes (0x45, 0x46) from current address
data_buf = i2c.read(addr, 2);
data = Uint8Array(data_buf);
print("Read bytes: " + data.length.toString());
for (let i = 0; i < data.length; i++) {
print("data[" + i.toString() + "] = " + data[i].toString(16));
}
}

View File

@@ -1,47 +0,0 @@
checkSdkFeatures(["infrared-send"]);
let infrared = require("infrared");
print("Sending Samsung32 signal (lowers volume)...");
infrared.sendSignal("Samsung32", 0x00000007, 0x0000000b);
delay(1000);
print("Sending raw signal... (Fujitsu AC)");
infrared.sendRawSignal(
[
3298, 1571, 442, 368, 442, 367, 443, 1180, 442, 370, 440, 1181, 442, 368,
442, 367, 442, 368, 442, 1180, 443, 1180, 442, 368, 441, 367, 442, 369, 441,
1180, 442, 1180, 443, 368, 442, 369, 441, 368, 442, 368, 442, 368, 442, 368,
441, 368, 441, 368, 442, 368, 442, 368, 442, 367, 442, 368, 442, 366, 444,
1181, 442, 368, 441, 367, 442, 368, 442, 367, 442, 369, 441, 367, 443, 367,
442, 1180, 442, 368, 442, 368, 442, 368, 441, 368, 441, 1180, 442, 1180,
442, 1181, 442, 1181, 441, 1181, 442, 1181, 442, 1181, 442, 1181, 442, 369,
441, 368, 441, 1182, 442, 367, 443, 367, 443, 367, 442, 368, 442, 1181, 441,
369, 441, 369, 441, 369, 441, 1180, 442, 1181, 441, 369, 442, 368, 442, 367,
442, 368, 441, 1181, 441, 1183, 440, 368, 442, 368, 441, 369, 441, 1181,
442, 368, 442, 367, 443, 1181, 442, 368, 442, 369, 441, 368, 441, 369, 440,
368, 442, 367, 443, 367, 442, 367, 442, 367, 442, 368, 442, 369, 441, 369,
441, 368, 442, 369, 441, 368, 441, 367, 443, 368, 442, 367, 442, 369, 441,
369, 441, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 368,
442, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 369, 441,
368, 441, 367, 442, 368, 441, 368, 442, 368, 442, 368, 442, 368, 442, 367,
442, 1181, 442, 367, 442, 368, 441, 1181, 442, 1182, 441, 1181, 442, 1181,
442, 1181, 441, 368, 442, 368, 442, 369, 441,
],
true,
{ frequency: 38000, dutyCycle: 0.33 },
);
delay(1000);
print(
"Sending raw signal... (Fujitsu AC) with default frequency and duty cycle",
);
infrared.sendRawSignal([
3300, 1596, 416, 362, 448, 363, 446, 1177, 445, 363, 446, 1177, 445, 362, 448,
362, 448, 364, 446, 1178, 444, 1207, 415, 362, 448, 362, 448, 363, 447, 1177,
445, 1177, 446, 362, 448, 362, 447, 362, 447, 362, 448, 363, 447, 362, 447,
363, 447, 363, 447, 363, 446, 363, 446, 362, 447, 362, 447, 363, 446, 1177,
445, 363, 447, 364, 446, 362, 448, 363, 447, 363, 446, 362, 447, 362, 448,
1175, 447, 363, 447, 364, 446, 362, 448, 362, 448, 1176, 446, 362, 448, 362,
448, 363, 446, 362, 448, 362, 448, 363, 447, 1175, 446, 394, 415, 1176, 446,
1178, 444, 1174, 449, 1177, 445, 1180, 443, 1179, 443,
]);
print("Success");

View File

@@ -1,93 +0,0 @@
let eventLoop = require("event_loop");
let gui = require("gui");
let dialog = require("gui/dialog");
let textInput = require("gui/text_input");
let loading = require("gui/loading");
let storage = require("storage");
// No eval() or exec() so need to run code from file, and filename must be unique
storage.makeDirectory("/ext/.tmp");
storage.makeDirectory("/ext/.tmp/js");
storage.rmrf("/ext/.tmp/js/repl")
storage.makeDirectory("/ext/.tmp/js/repl")
let ctx = {
tmpTemplate: "/ext/.tmp/js/repl/",
tmpNumber: 0,
persistentScope: {},
};
let views = {
dialog: dialog.makeWith({
header: "Interactive Console",
text: "Press OK to Start",
center: "Run Some JS"
}),
textInput: textInput.makeWith({
header: "Type JavaScript Code:",
minLength: 0,
maxLength: 256,
defaultText: "2+2",
defaultTextClear: true,
}),
loading: loading.make(),
};
eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) {
if (button === "center") {
gui.viewDispatcher.switchTo(views.textInput);
}
}, gui, views);
eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) {
gui.viewDispatcher.switchTo(views.loading);
let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString();
let file = storage.openFile(path, "w", "create_always");
file.write(text);
file.close();
// Hide GUI before running, we want to see console and avoid deadlock if code fails
gui.viewDispatcher.sendTo("back");
let result = load(path, ctx.persistentScope); // Load runs JS and returns last value on stack
storage.remove(path);
// Must convert to string explicitly
if (result === null) { // mJS: typeof null === "null", ECMAScript: typeof null === "object", IDE complains when checking "null" type
result = "null";
} else if (typeof result === "string") {
result = "'" + result + "'";
} else if (typeof result === "number") {
result = result.toString();
} else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check
result = "bigint";
} else if (typeof result === "boolean") {
result = result ? "true" : "false";
} else if (typeof result === "symbol") { // mJS doesn't support Symbol() but might aswell check
result = "symbol";
} else if (typeof result === "undefined") {
result = "undefined";
} else if (typeof result === "object") {
result = "object"; // JSON.stringify() is not implemented
} else if (typeof result === "function") {
result = "function";
} else {
result = "unknown type: " + typeof result;
}
gui.viewDispatcher.sendTo("front");
views.dialog.set("header", "JS Returned:");
views.dialog.set("text", result);
gui.viewDispatcher.switchTo(views.dialog);
views.textInput.set("defaultText", text);
}, gui, views, ctx);
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) {
eventLoop.stop();
}, eventLoop);
gui.viewDispatcher.switchTo(views.dialog);
// Message behind GUI if something breaks
print("If you're stuck here, something went wrong, re-run the script")
eventLoop.run();
print("\n\nFinished correctly :)")

View File

@@ -1,3 +0,0 @@
let math = load(__dirname + "/load_api.js");
let result = math.add(5, 10);
print(result);

View File

@@ -1,3 +0,0 @@
({
add: function (a, b) { return a + b; },
})

View File

@@ -1,24 +0,0 @@
let math = require("math");
print("math.abs(-5):", math.abs(-5));
print("math.acos(0.5):", math.acos(0.5));
print("math.acosh(2):", math.acosh(2));
print("math.asin(0.5):", math.asin(0.5));
print("math.asinh(2):", math.asinh(2));
print("math.atan(1):", math.atan(1));
print("math.atan2(1, 1):", math.atan2(1, 1));
print("math.atanh(0.5):", math.atanh(0.5));
print("math.cbrt(27):", math.cbrt(27));
print("math.ceil(5.3):", math.ceil(5.3));
print("math.clz32(1):", math.clz32(1));
print("math.cos(math.PI):", math.cos(math.PI));
print("math.exp(1):", math.exp(1));
print("math.floor(5.7):", math.floor(5.7));
print("math.max(3, 5):", math.max(3, 5));
print("math.min(3, 5):", math.min(3, 5));
print("math.pow(2, 3):", math.pow(2, 3));
print("math.random():", math.random());
print("math.sign(-5):", math.sign(-5));
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
print("math.sqrt(25):", math.sqrt(25));
print("math.trunc(5.7):", math.trunc(5.7));

View File

@@ -1,9 +0,0 @@
let notify = require("notification");
notify.error();
delay(1000);
notify.success();
delay(1000);
for (let i = 0; i < 10; i++) {
notify.blink("red", "short");
delay(500);
}

View File

@@ -1,9 +0,0 @@
let storage = require("storage");
print("script has __dirname of" + __dirname);
print("script has __filename of" + __filename);
if (storage.fileExists(__dirname + "/math.js")) {
print("math.js exist here.");
} else {
print("math.js does not exist here.");
}

View File

@@ -1,92 +0,0 @@
// Script cannot work without spi module so check before
checkSdkFeatures(["spi"]);
// 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 textBoxView = require("gui/text_box");
let text = "SPI demo\n";
let textBox = textBoxView.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();

View File

@@ -1,29 +0,0 @@
let storage = require("storage");
let path = "/ext/storage.test";
print("File exists:", storage.fileExists(path));
print("Writing...");
let file = storage.openFile(path, "w", "create_always");
file.write("Hello ");
file.close();
print("File exists:", storage.fileExists(path));
file = storage.openFile(path, "w", "open_append");
file.write("World!");
file.close();
print("Reading...");
file = storage.openFile(path, "r", "open_existing");
let text = file.read("ascii", 128);
file.close();
print(text);
print("Removing...")
storage.remove(path);
print("Done")
// You don't need to close the file after each operation, this is just to show some different ways to use the API
// There's also many more functions and options, check type definitions in firmware repo

View File

@@ -1,19 +0,0 @@
let sampleText = "Hello, World!";
let lengthOfText = "Length of text: " + sampleText.length.toString();
print(lengthOfText);
let start = 7;
let end = 12;
let substringResult = sampleText.slice(start, end);
print(substringResult);
let searchStr = "World";
let result2 = sampleText.indexOf(searchStr).toString();
print(result2);
let upperCaseText = "Text in upper case: " + sampleText.toUpperCase();
print(upperCaseText);
let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase();
print(lowerCaseText);

View File

@@ -1,48 +0,0 @@
// Script cannot work without subghz module so check before
checkSdkFeatures(["subghz"]);
let subghz = require("subghz");
subghz.setup();
function printRXline() {
if (subghz.getState() !== "RX") {
subghz.setRx(); // to RX
}
let rssi = subghz.getRssi();
let freq = subghz.getFrequency();
let ext = subghz.isExternal();
print("rssi: ", rssi, "dBm", "@", freq, "MHz", "ext: ", ext);
}
function changeFrequency(freq) {
if (subghz.getState() !== "IDLE") {
subghz.setIdle(); // need to be idle to change frequency
}
subghz.setFrequency(freq);
}
subghz.setIdle();
print(subghz.getState()); // "IDLE"
subghz.setRx();
print(subghz.getState()); // "RX"
changeFrequency(433920000);
printRXline();
delay(1000);
print("Sending 0.sub")
subghz.transmitFile("/ext/subghz/0.sub");
// Can also specify repeat count: subghz.transmitFile(path, repeat)
// If not provided, defaults to 1 repeat for RAW and 10 repeats for parsed
// These 10 repeats by default are to simulate holding the button on remote
print("Send success");
delay(1000);
changeFrequency(315000000);
printRXline();
// Optional, done automatically at script end
subghz.end()
// But can be used to setup again, which will retry to detect external modules

View File

@@ -1,14 +0,0 @@
let serial = require("serial");
serial.setup("usart", 230400);
while (1) {
let rx_data = serial.readBytes(1, 1000);
if (rx_data !== undefined) {
serial.write(rx_data);
let data_view = Uint8Array(rx_data);
print("0x" + data_view[0].toString(16));
}
}
// There's also serial.end(), so you can serial.setup() again in same script
// You can also use serial.readAny(timeout), will avoid starving your loop with single byte reads

View File

@@ -1,15 +0,0 @@
// This script is like uart_echo, except it uses 8E1 framing (8 data bits, even
// parity, 1 stop bit) as opposed to the default 8N1 (8 data bits, no parity,
// 1 stop bit)
let serial = require("serial");
serial.setup("usart", 230400, { dataBits: "8", parity: "even", stopBits: "1" });
while (1) {
let rx_data = serial.readBytes(1, 1000);
if (rx_data !== undefined) {
serial.write(rx_data);
let data_view = Uint8Array(rx_data);
print("0x" + data_view[0].toString(16));
}
}

View File

@@ -1,39 +0,0 @@
// Script cannot work without usbdisk module so check before
checkSdkFeatures(["usbdisk"]);
let usbdisk = require("usbdisk");
let storage = require("storage");
let imagePath = "/ext/apps_data/mass_storage/128MB.img";
let imageSize = 128 * 1024 * 1024;
let imageExisted = storage.fileExists(imagePath);
if (imageExisted) {
print("Disk image '128MB' already exists");
} else {
// CreateImage isn't necessary to overall function, check when its used not at script start
if (doesSdkSupport(["usbdisk-createimage"])) {
print("Creating disk image '128MB'...");
usbdisk.createImage(imagePath, imageSize);
} else {
die("Disk image '128MB' not present, can't auto-create");
}
}
print("Starting UsbDisk...");
usbdisk.start("/ext/apps_data/mass_storage/128MB.img");
print("Started, waiting until ejected...");
while (!usbdisk.wasEjected()) {
delay(1000);
}
print("Ejected, stopping UsbDisk...");
usbdisk.stop();
if (!imageExisted) {
print("Removing disk image...");
storage.remove(imagePath);
}
print("Done");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

View File

@@ -1,217 +0,0 @@
#include <dialogs/dialogs.h>
#include "js_thread.h"
#include <storage/storage.h>
#include "js_app_i.h"
#include <toolbox/path.h>
#include <assets_icons.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <toolbox/pipe.h>
#define TAG "JS app"
typedef struct {
JsThread* js_thread;
Gui* gui;
ViewDispatcher* view_dispatcher;
Loading* loading;
JsConsoleView* console_view;
} JsApp;
static uint32_t js_view_exit(void* context) {
UNUSED(context);
return VIEW_NONE;
}
static void js_app_compact_trace(FuriString* trace_str) {
// Keep only first line
size_t line_end = furi_string_search_char(trace_str, '\n');
if(line_end > 0) {
furi_string_left(trace_str, line_end);
}
// Remove full path
FuriString* file_name = furi_string_alloc();
size_t filename_start = furi_string_search_rchar(trace_str, '/');
if(filename_start > 0) {
filename_start++;
furi_string_set_n(
file_name, trace_str, filename_start, furi_string_size(trace_str) - filename_start);
furi_string_printf(trace_str, "at %s", furi_string_get_cstr(file_name));
}
furi_string_free(file_name);
}
static void js_callback(JsThreadEvent event, const char* msg, void* context) {
JsApp* app = context;
furi_assert(app);
if(event == JsThreadEventDone) {
FURI_LOG_I(TAG, "Script done");
console_view_print(app->console_view, "--- DONE ---");
} else if(event == JsThreadEventPrint) {
console_view_print(app->console_view, msg);
} else if(event == JsThreadEventError) {
console_view_print(app->console_view, "--- ERROR ---");
console_view_print(app->console_view, msg);
} else if(event == JsThreadEventErrorTrace) {
FuriString* compact_trace = furi_string_alloc_set_str(msg);
js_app_compact_trace(compact_trace);
console_view_print(app->console_view, furi_string_get_cstr(compact_trace));
furi_string_free(compact_trace);
console_view_print(app->console_view, "See logs for full trace");
}
}
static JsApp* js_app_alloc(void) {
JsApp* app = malloc(sizeof(JsApp));
app->view_dispatcher = view_dispatcher_alloc();
app->loading = loading_alloc();
app->gui = furi_record_open("gui");
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
view_dispatcher_add_view(
app->view_dispatcher, JsAppViewLoading, loading_get_view(app->loading));
app->console_view = console_view_alloc();
view_dispatcher_add_view(
app->view_dispatcher, JsAppViewConsole, console_view_get_view(app->console_view));
view_set_previous_callback(console_view_get_view(app->console_view), js_view_exit);
view_dispatcher_switch_to_view(app->view_dispatcher, JsAppViewConsole);
return app;
}
static void js_app_free(JsApp* app) {
console_view_free(app->console_view);
view_dispatcher_remove_view(app->view_dispatcher, JsAppViewConsole);
loading_free(app->loading);
view_dispatcher_remove_view(app->view_dispatcher, JsAppViewLoading);
view_dispatcher_free(app->view_dispatcher);
furi_record_close("gui");
free(app);
}
int32_t js_app(void* arg) {
JsApp* app = js_app_alloc();
FuriString* script_path = furi_string_alloc_set(EXT_PATH("apps/Scripts"));
do {
if(arg != NULL && strlen(arg) > 0) {
furi_string_set(script_path, (const char*)arg);
} else {
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(&browser_options, ".js", &I_js_script_10px);
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
if(!dialog_file_browser_show(dialogs, script_path, script_path, &browser_options))
break;
furi_record_close(RECORD_DIALOGS);
}
FuriString* name = furi_string_alloc();
path_extract_filename(script_path, name, false);
FuriString* start_text =
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
console_view_print(app->console_view, furi_string_get_cstr(start_text));
console_view_print(app->console_view, "-------------");
furi_string_free(name);
furi_string_free(start_text);
app->js_thread = js_thread_run(furi_string_get_cstr(script_path), js_callback, app);
view_dispatcher_run(app->view_dispatcher);
js_thread_stop(app->js_thread);
} while(0);
furi_string_free(script_path);
js_app_free(app);
return 0;
} //-V773
typedef struct {
PipeSide* pipe;
FuriSemaphore* exit_sem;
} JsCliContext;
static void js_cli_print(JsCliContext* ctx, const char* msg) {
UNUSED(ctx);
UNUSED(msg);
pipe_send(ctx->pipe, msg, strlen(msg));
}
static void js_cli_exit(JsCliContext* ctx) {
furi_check(furi_semaphore_release(ctx->exit_sem) == FuriStatusOk);
}
static void js_cli_callback(JsThreadEvent event, const char* msg, void* context) {
JsCliContext* ctx = context;
switch(event) {
case JsThreadEventError:
js_cli_print(ctx, "---- ERROR ----\r\n");
js_cli_print(ctx, msg);
js_cli_print(ctx, "\r\n");
break;
case JsThreadEventErrorTrace:
js_cli_print(ctx, "Trace:\r\n");
js_cli_print(ctx, msg);
js_cli_print(ctx, "\r\n");
js_cli_exit(ctx); // Exit when an error occurs
break;
case JsThreadEventPrint:
js_cli_print(ctx, msg);
js_cli_print(ctx, "\r\n");
break;
case JsThreadEventDone:
js_cli_print(ctx, "Script done!\r\n");
js_cli_exit(ctx);
break;
}
}
void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(context);
const char* path = furi_string_get_cstr(args);
Storage* storage = furi_record_open(RECORD_STORAGE);
do {
if(furi_string_size(args) == 0) {
printf("Usage:\r\njs <path>\r\n");
break;
}
if(!storage_file_exists(storage, path)) {
printf("Can not open file %s\r\n", path);
break;
}
JsCliContext ctx = {.pipe = pipe};
ctx.exit_sem = furi_semaphore_alloc(1, 0);
printf("Running script %s, press CTRL+C to stop\r\n", path);
JsThread* js_thread = js_thread_run(path, js_cli_callback, &ctx);
while(furi_semaphore_acquire(ctx.exit_sem, 100) != FuriStatusOk) {
if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break;
}
js_thread_stop(js_thread);
furi_semaphore_free(ctx.exit_sem);
} while(false);
furi_record_close(RECORD_STORAGE);
}
void js_app_on_system_start(void) {
#ifdef SRV_CLI
CliRegistry* registry = furi_record_open(RECORD_CLI);
cli_registry_add_command(registry, "js", CliCommandFlagDefault, js_cli_execute, NULL);
furi_record_close(RECORD_CLI);
#endif
}

View File

@@ -1,10 +0,0 @@
#include <furi.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/loading.h>
#include "views/console_view.h"
typedef enum {
JsAppViewConsole,
JsAppViewLoading,
} JsAppView;

View File

@@ -1,338 +0,0 @@
#include <core/common_defines.h>
#include "js_modules.h"
#include <m-array.h>
#include <dialogs/dialogs.h>
#include <assets_icons.h>
#include "modules/js_flipper.h"
#ifdef FW_CFG_unit_tests
#include "modules/js_tests.h"
#endif
#define TAG "JS modules"
// Absolute path is used to make possible plugin load from CLI
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
typedef struct {
FuriString* name;
const JsModuleConstructor create;
const JsModuleDestructor destroy;
void* context;
} JsModuleData;
// not using:
// - a dict because ordering is required
// - a bptree because it forces a sorted ordering
// - an rbtree because i deemed it more tedious to implement, and with the
// amount of modules in use (under 10 in the overwhelming majority of cases)
// i bet it's going to be slower than a plain array
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST); //-V658
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
static const JsModuleDescriptor modules_builtin[] = {
{"flipper", js_flipper_create, NULL, NULL},
#ifdef FW_CFG_unit_tests
{"tests", js_tests_create, NULL, NULL},
#endif
};
struct JsModules {
struct mjs* mjs;
JsModuleArray_t modules;
PluginManager* plugin_manager;
CompositeApiResolver* resolver;
};
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
JsModules* modules = malloc(sizeof(JsModules));
modules->mjs = mjs;
JsModuleArray_init(modules->modules);
modules->plugin_manager = plugin_manager_alloc(
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
modules->resolver = resolver;
return modules;
}
void js_modules_destroy(JsModules* instance) {
for
M_EACH(module, instance->modules, JsModuleArray_t) {
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
if(module->destroy) module->destroy(module->context);
furi_string_free(module->name);
}
plugin_manager_free(instance->plugin_manager);
JsModuleArray_clear(instance->modules);
free(instance);
}
JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) {
for
M_EACH(module, instance->modules, JsModuleArray_t) {
if(furi_string_cmp_str(module->name, name) == 0) return module;
}
return NULL;
}
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) {
// Ignore the initial part of the module name
const char* optional_module_prefix = "@" JS_SDK_VENDOR "/fz-sdk/";
if(strncmp(name, optional_module_prefix, strlen(optional_module_prefix)) == 0) {
name += strlen(optional_module_prefix);
}
// Check if module is already installed
JsModuleData* module_inst = js_find_loaded_module(modules, name);
if(module_inst) { //-V547
mjs_prepend_errorf(
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
return MJS_UNDEFINED;
}
bool module_found = false;
// Check built-in modules
for(size_t i = 0; i < COUNT_OF(modules_builtin); i++) { //-V1008
size_t name_compare_len = strlen(modules_builtin[i].name);
if(name_compare_len != name_len) {
continue;
}
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
JsModuleData module = {
.create = modules_builtin[i].create,
.destroy = modules_builtin[i].destroy,
.name = furi_string_alloc_set_str(name),
};
JsModuleArray_push_at(modules->modules, 0, module);
module_found = true;
FURI_LOG_I(TAG, "Using built-in module %s", name);
break;
}
}
// External module load
if(!module_found) {
FuriString* deslashed_name = furi_string_alloc_set_str(name);
furi_string_replace_all_str(deslashed_name, "/", "__");
FuriString* module_path = furi_string_alloc();
furi_string_printf(
module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name));
FURI_LOG_I(
TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path));
do {
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
PluginManagerError load_error = plugin_manager_load_single(
modules->plugin_manager, furi_string_get_cstr(module_path));
if(load_error != PluginManagerErrorNone) {
FURI_LOG_E(
TAG,
"Module %s load error. It may depend on other modules that are not yet loaded.",
name);
break;
}
const JsModuleDescriptor* plugin =
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
furi_assert(plugin);
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
break;
}
JsModuleData module = {
.create = plugin->create,
.destroy = plugin->destroy,
.name = furi_string_alloc_set_str(name),
};
JsModuleArray_push_at(modules->modules, 0, module);
if(plugin->api_interface) {
FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name);
composite_api_resolver_add(modules->resolver, plugin->api_interface);
}
module_found = true;
} while(0);
furi_string_free(module_path);
furi_string_free(deslashed_name);
}
// Run module constructor
mjs_val_t module_object = MJS_UNDEFINED;
if(module_found) {
module_inst = js_find_loaded_module(modules, name);
furi_assert(module_inst);
if(module_inst->create) { //-V779
module_inst->context = module_inst->create(modules->mjs, &module_object, modules);
}
}
if(module_object == MJS_UNDEFINED) { //-V547
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
}
return module_object;
}
void* js_module_get(JsModules* modules, const char* name) {
FuriString* module_name = furi_string_alloc_set_str(name);
JsModuleData* module_inst = js_find_loaded_module(modules, name);
furi_string_free(module_name);
return module_inst ? module_inst->context : NULL;
}
typedef enum {
JsSdkCompatStatusCompatible,
JsSdkCompatStatusFirmwareTooOld,
JsSdkCompatStatusFirmwareTooNew,
} JsSdkCompatStatus;
/**
* @brief Checks compatibility between the firmware and the JS SDK version
* expected by the script
*/
static JsSdkCompatStatus
js_internal_sdk_compatibility_status(int32_t exp_major, int32_t exp_minor) {
if(exp_major < JS_SDK_MAJOR) return JsSdkCompatStatusFirmwareTooNew;
if(exp_major > JS_SDK_MAJOR || exp_minor > JS_SDK_MINOR)
return JsSdkCompatStatusFirmwareTooOld;
return JsSdkCompatStatusCompatible;
}
static const JsValueDeclaration js_sdk_version_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_sdk_version_args = JS_VALUE_ARGS(js_sdk_version_arg_list);
void js_sdk_compatibility_status(struct mjs* mjs) {
int32_t major, minor;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor);
JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor);
switch(status) {
case JsSdkCompatStatusCompatible:
mjs_return(mjs, mjs_mk_string(mjs, "compatible", ~0, 0));
return;
case JsSdkCompatStatusFirmwareTooOld:
mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooOld", ~0, 0));
return;
case JsSdkCompatStatusFirmwareTooNew:
mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooNew", ~0, 0));
return;
}
}
void js_is_sdk_compatible(struct mjs* mjs) {
int32_t major, minor;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor);
JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor);
mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible));
}
/**
* @brief Asks the user whether to continue executing an incompatible script
*/
static bool js_internal_compat_ask_user(const char* message) {
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogMessage* dialog = dialog_message_alloc();
dialog_message_set_header(dialog, message, 64, 0, AlignCenter, AlignTop);
dialog_message_set_text(
dialog, "This script may not\nwork as expected", 79, 32, AlignCenter, AlignCenter);
dialog_message_set_icon(dialog, &I_Warning_30x23, 0, 18);
dialog_message_set_buttons(dialog, "Go back", NULL, "Run anyway");
DialogMessageButton choice = dialog_message_show(dialogs, dialog);
dialog_message_free(dialog);
furi_record_close(RECORD_DIALOGS);
return choice == DialogMessageButtonRight;
}
void js_check_sdk_compatibility(struct mjs* mjs) {
int32_t major, minor;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor);
JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor);
if(status != JsSdkCompatStatusCompatible) {
FURI_LOG_E(
TAG,
"Script requests JS SDK %ld.%ld, firmware provides JS SDK %d.%d",
major,
minor,
JS_SDK_MAJOR,
JS_SDK_MINOR);
const char* message = (status == JsSdkCompatStatusFirmwareTooOld) ? "Outdated Firmware" :
"Outdated Script";
if(!js_internal_compat_ask_user(message)) {
JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script");
}
}
}
static const char* extra_features[] = {
"baseline", // dummy "feature"
"gpio-pwm",
"gui-widget",
"serial-framing",
"gui-widget-extras",
// extra modules
"blebeacon",
"i2c",
"spi",
"infrared-send",
"subghz",
"usbdisk",
"vgm",
};
/**
* @brief Determines whether a feature is supported
*/
static bool js_internal_supports(const char* feature) {
for(size_t i = 0; i < COUNT_OF(extra_features); i++) { // -V1008
if(strcmp(feature, extra_features[i]) == 0) return true;
}
return false;
}
/**
* @brief Determines whether all of the requested features are supported
*/
static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) {
furi_assert(mjs_is_array(feature_arr));
for(size_t i = 0; i < mjs_array_length(mjs, feature_arr); i++) {
mjs_val_t feature = mjs_array_get(mjs, feature_arr, i);
const char* feature_str = mjs_get_string(mjs, &feature, NULL);
if(!feature_str) return false;
if(!js_internal_supports(feature_str)) return false;
}
return true;
}
static const JsValueDeclaration js_sdk_features_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAnyArray),
};
static const JsValueArguments js_sdk_features_args = JS_VALUE_ARGS(js_sdk_features_arg_list);
void js_does_sdk_support(struct mjs* mjs) {
mjs_val_t features;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features);
mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features)));
}
void js_check_sdk_features(struct mjs* mjs) {
mjs_val_t features;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features);
if(!js_internal_supports_all_of(mjs, features)) {
FURI_LOG_E(TAG, "Script requests unsupported features");
if(!js_internal_compat_ask_user("Unsupported Feature")) {
JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script");
}
}
}

View File

@@ -1,149 +0,0 @@
#pragma once
#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>
#ifdef __cplusplus
extern "C" {
#endif
#define PLUGIN_APP_ID "js"
#define PLUGIN_API_VERSION 1
#define JS_SDK_VENDOR_FIRMWARE "unleashed"
#define JS_SDK_VENDOR "flipperdevices"
#define JS_SDK_MAJOR 1
#define JS_SDK_MINOR 0
/**
* @brief Returns the foreign pointer in `obj["_"]`
*/
#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0))
/**
* @brief Returns the foreign pointer in `this["_"]`
*/
#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs))
/**
* @brief Syntax sugar for constructing an object
*
* Example:
*
* mjs_val_t my_obj = mjs_mk_object(mjs);
* JS_ASSIGN_MULTI(mjs, my_obj) {
* JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open));
* JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open));
* }
*/
#define JS_ASSIGN_MULTI(mjs, object) \
for(struct { \
struct mjs* mjs; \
mjs_val_t val; \
int i; \
} _ass_multi = {mjs, object, 0}; \
_ass_multi.i == 0; \
_ass_multi.i++)
#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value)
/**
* @brief The first word of structures that foreign pointer JS values point to
*
* This is used to detect situations where JS code mistakenly passes an opaque
* foreign pointer of one type as an argument to a native function which expects
* a struct of another type.
*
* It is recommended to use this functionality in conjunction with the following
* convenience verification macros:
* - `JS_ARG_STRUCT()`
* - `JS_ARG_OBJ_WITH_STRUCT()`
*
* @warning In order for the mechanism to work properly, your struct must store
* the magic value in the first word.
*/
typedef enum {
JsForeignMagicStart = 0x15BAD000,
JsForeignMagic_JsEventLoopContract,
} JsForeignMagic;
/**
* @brief Prepends an error, sets the JS return value to `undefined` and returns
* from the C function
* @warning This macro executes `return;` by design
*/
#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \
do { \
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
mjs_return(mjs, MJS_UNDEFINED); \
return; \
} while(0)
/**
* @brief Prepends an error, sets the JS return value to `undefined` and returns
* a value C function
* @warning This macro executes `return;` by design
*/
#define JS_ERROR_AND_RETURN_VAL(mjs, error_code, ret_val, ...) \
do { \
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
mjs_return(mjs, MJS_UNDEFINED); \
return ret_val; \
} while(0)
typedef struct JsModules JsModules;
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
typedef void (*JsModuleDestructor)(void* inst);
typedef struct {
char* name;
JsModuleConstructor create;
JsModuleDestructor destroy;
const ElfApiInterface* api_interface;
} JsModuleDescriptor;
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
void js_modules_destroy(JsModules* modules);
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
/**
* @brief Gets a module instance by its name
* This is useful when a module wants to access a stateful API of another
* module.
* @returns Pointer to module context, NULL if the module is not instantiated
*/
void* js_module_get(JsModules* modules, const char* name);
/**
* @brief `sdkCompatibilityStatus` function
*/
void js_sdk_compatibility_status(struct mjs* mjs);
/**
* @brief `isSdkCompatible` function
*/
void js_is_sdk_compatible(struct mjs* mjs);
/**
* @brief `checkSdkCompatibility` function
*/
void js_check_sdk_compatibility(struct mjs* mjs);
/**
* @brief `doesSdkSupport` function
*/
void js_does_sdk_support(struct mjs* mjs);
/**
* @brief `checkSdkFeatures` function
*/
void js_check_sdk_features(struct mjs* mjs);
#ifdef __cplusplus
}
#endif

View File

@@ -1,357 +0,0 @@
#include <common/cs_dbg.h>
#include <toolbox/path.h>
#include <toolbox/stream/file_stream.h>
#include <toolbox/strint.h>
#include <loader/firmware_api/firmware_api.h>
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/plugins/composite_resolver.h>
#include <furi_hal.h>
#include "plugin_api/app_api_interface.h"
#include "js_thread.h"
#include "js_thread_i.h"
#include "js_modules.h"
#define TAG "JS"
struct JsThread {
FuriThread* thread;
FuriString* path;
CompositeApiResolver* resolver;
JsThreadCallback app_callback;
void* context;
JsModules* modules;
};
static void js_str_print(FuriString* msg_str, struct mjs* mjs) {
size_t num_args = mjs_nargs(mjs);
for(size_t i = 0; i < num_args; i++) {
char* name = NULL;
size_t name_len = 0;
int need_free = 0;
mjs_val_t arg = mjs_arg(mjs, i);
mjs_err_t err = mjs_to_string(mjs, &arg, &name, &name_len, &need_free);
if(err != MJS_OK) {
furi_string_cat_printf(msg_str, "err %s ", mjs_strerror(mjs, err));
} else {
furi_string_cat_printf(msg_str, "%s ", name);
}
if(need_free) {
free(name);
name = NULL;
}
}
}
static void js_print(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
JsThread* worker = mjs_get_context(mjs);
furi_assert(worker);
if(worker->app_callback) {
worker->app_callback(JsThreadEventPrint, furi_string_get_cstr(msg_str), worker->context);
} else {
FURI_LOG_D(TAG, "%s\r\n", furi_string_get_cstr(msg_str));
}
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_log(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_I(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_warn(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_W(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_error(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_E(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_console_debug(struct mjs* mjs) {
FuriString* msg_str = furi_string_alloc();
js_str_print(msg_str, mjs);
FURI_LOG_D(TAG, "%s", furi_string_get_cstr(msg_str));
furi_string_free(msg_str);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_exit_flag_poll(struct mjs* mjs) {
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
if(flags & FuriFlagError) {
return;
}
if(flags & ThreadEventStop) {
mjs_exit(mjs);
}
}
bool js_delay_with_flags(struct mjs* mjs, uint32_t time) {
uint32_t flags =
furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, time);
if(flags & FuriFlagError) {
return false;
}
if(flags & ThreadEventStop) {
mjs_exit(mjs);
return true;
}
return false;
}
void js_flags_set(struct mjs* mjs, uint32_t flags) {
JsThread* worker = mjs_get_context(mjs);
furi_assert(worker);
furi_thread_flags_set(furi_thread_get_id(worker->thread), flags);
}
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags_mask, uint32_t timeout) {
flags_mask |= ThreadEventStop;
uint32_t flags = furi_thread_flags_get();
furi_check((flags & FuriFlagError) == 0);
if(flags == 0) {
flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny | FuriFlagNoClear, timeout);
} else {
uint32_t state = furi_thread_flags_clear(flags & flags_mask);
furi_check((state & FuriFlagError) == 0);
}
if(flags & FuriFlagError) {
return 0;
}
if(flags & ThreadEventStop) {
mjs_exit(mjs);
}
return flags;
}
static void js_delay(struct mjs* mjs) {
bool args_correct = false;
int ms = 0;
if(mjs_nargs(mjs) == 1) {
mjs_val_t arg = mjs_arg(mjs, 0);
if(mjs_is_number(arg)) {
ms = mjs_get_int(mjs, arg);
args_correct = true;
}
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_delay_with_flags(mjs, ms);
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_dlsym(void* handle, const char* name) {
CompositeApiResolver* resolver = handle;
Elf32_Addr addr = 0;
uint32_t hash = elf_symbolname_hash(name);
const ElfApiInterface* api = composite_api_resolver_get(resolver);
if(!api->resolver_callback(api, hash, &addr)) {
FURI_LOG_E(TAG, "FFI: cannot find \"%s\"", name);
return NULL;
}
return (void*)addr;
}
static void js_ffi_address(struct mjs* mjs) {
mjs_val_t name_v = mjs_arg(mjs, 0);
size_t len;
const char* name = mjs_get_string(mjs, &name_v, &len);
void* addr = mjs_ffi_resolve(mjs, name);
mjs_return(mjs, mjs_mk_foreign(mjs, addr));
}
static void js_require(struct mjs* mjs) {
mjs_val_t name_v = mjs_arg(mjs, 0);
size_t len;
const char* name = mjs_get_string(mjs, &name_v, &len);
mjs_val_t req_object = MJS_UNDEFINED;
if((len == 0) || (name == NULL)) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "String argument is expected");
} else {
JsThread* worker = mjs_get_context(mjs);
furi_assert(worker);
req_object = js_module_require(worker->modules, name, len);
}
mjs_return(mjs, req_object);
}
static void js_parse_int(struct mjs* mjs) {
static const JsValueDeclaration js_parse_int_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 10),
};
static const JsValueArguments js_parse_int_args = JS_VALUE_ARGS(js_parse_int_arg_list);
const char* str;
int32_t base;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_parse_int_args, &str, &base);
int32_t num;
if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) {
num = 0;
}
mjs_return(mjs, mjs_mk_number(mjs, num));
}
#ifdef JS_DEBUG
static void js_dump_write_callback(void* ctx, const char* format, ...) {
File* file = ctx;
furi_assert(ctx);
FuriString* str = furi_string_alloc();
va_list args;
va_start(args, format);
furi_string_vprintf(str, format, args);
furi_string_cat(str, "\n");
va_end(args);
storage_file_write(file, furi_string_get_cstr(str), furi_string_size(str));
furi_string_free(str);
}
#endif
static int32_t js_thread(void* arg) {
JsThread* worker = arg;
worker->resolver = composite_api_resolver_alloc();
composite_api_resolver_add(worker->resolver, firmware_api_interface);
composite_api_resolver_add(worker->resolver, application_api_interface);
struct mjs* mjs = mjs_create(worker);
worker->modules = js_modules_create(mjs, worker->resolver);
mjs_val_t global = mjs_get_global(mjs);
mjs_val_t console_obj = mjs_mk_object(mjs);
if(worker->path) {
FuriString* dirpath = furi_string_alloc();
path_extract_dirname(furi_string_get_cstr(worker->path), dirpath);
mjs_set(
mjs,
global,
"__filename",
~0,
mjs_mk_string(
mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true));
mjs_set(
mjs,
global,
"__dirname",
~0,
mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true));
furi_string_free(dirpath);
}
JS_ASSIGN_MULTI(mjs, global) {
JS_FIELD("print", MJS_MK_FN(js_print));
JS_FIELD("delay", MJS_MK_FN(js_delay));
JS_FIELD("parseInt", MJS_MK_FN(js_parse_int));
JS_FIELD("ffi_address", MJS_MK_FN(js_ffi_address));
JS_FIELD("require", MJS_MK_FN(js_require));
JS_FIELD("console", console_obj);
JS_FIELD("sdkCompatibilityStatus", MJS_MK_FN(js_sdk_compatibility_status));
JS_FIELD("isSdkCompatible", MJS_MK_FN(js_is_sdk_compatible));
JS_FIELD("checkSdkCompatibility", MJS_MK_FN(js_check_sdk_compatibility));
JS_FIELD("doesSdkSupport", MJS_MK_FN(js_does_sdk_support));
JS_FIELD("checkSdkFeatures", MJS_MK_FN(js_check_sdk_features));
}
JS_ASSIGN_MULTI(mjs, console_obj) {
JS_FIELD("log", MJS_MK_FN(js_console_log));
JS_FIELD("warn", MJS_MK_FN(js_console_warn));
JS_FIELD("error", MJS_MK_FN(js_console_error));
JS_FIELD("debug", MJS_MK_FN(js_console_debug));
}
mjs_set_ffi_resolver(mjs, js_dlsym, worker->resolver);
mjs_set_exec_flags_poller(mjs, js_exit_flag_poll);
mjs_err_t err = mjs_exec_file(mjs, furi_string_get_cstr(worker->path), NULL);
#ifdef JS_DEBUG
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
FuriString* dump_path = furi_string_alloc_set(worker->path);
furi_string_cat(dump_path, ".lst");
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
if(storage_file_open(
file, furi_string_get_cstr(dump_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
mjs_disasm_all(mjs, js_dump_write_callback, file);
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
furi_string_free(dump_path);
}
#endif
if(err != MJS_OK) {
FURI_LOG_E(TAG, "Exec error: %s", mjs_strerror(mjs, err));
if(worker->app_callback) {
worker->app_callback(JsThreadEventError, mjs_strerror(mjs, err), worker->context);
}
const char* stack_trace = mjs_get_stack_trace(mjs);
if(stack_trace != NULL) {
FURI_LOG_E(TAG, "Stack trace:\r\n%s", stack_trace);
if(worker->app_callback) {
worker->app_callback(JsThreadEventErrorTrace, stack_trace, worker->context);
}
}
} else {
if(worker->app_callback) {
worker->app_callback(JsThreadEventDone, NULL, worker->context);
}
}
mjs_destroy(mjs);
js_modules_destroy(worker->modules);
composite_api_resolver_free(worker->resolver);
return 0;
}
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context) {
JsThread* worker = malloc(sizeof(JsThread)); //-V799
worker->path = furi_string_alloc_set(script_path);
worker->thread = furi_thread_alloc_ex("JsThread", 8 * 1024, js_thread, worker);
worker->app_callback = callback;
worker->context = context;
furi_thread_start(worker->thread);
return worker;
}
void js_thread_stop(JsThread* worker) {
furi_thread_flags_set(furi_thread_get_id(worker->thread), ThreadEventStop);
furi_thread_join(worker->thread);
furi_thread_free(worker->thread);
furi_string_free(worker->path);
free(worker);
}

View File

@@ -1,24 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct JsThread JsThread;
typedef enum {
JsThreadEventDone,
JsThreadEventError,
JsThreadEventPrint,
JsThreadEventErrorTrace,
} JsThreadEvent;
typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* context);
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
void js_thread_stop(JsThread* worker);
#ifdef __cplusplus
}
#endif

View File

@@ -1,33 +0,0 @@
#pragma once
#include <furi.h>
#include <mjs_core_public.h>
#include <mjs_ffi_public.h>
#include <mjs_exec_public.h>
#include <mjs_object_public.h>
#include <mjs_string_public.h>
#include <mjs_array_public.h>
#include <mjs_util_public.h>
#include <mjs_primitive_public.h>
#include <mjs_array_buf_public.h>
#ifdef __cplusplus
extern "C" {
#endif
#define INST_PROP_NAME "_"
typedef enum {
ThreadEventStop = (1 << 0),
ThreadEventCustomDataRx = (1 << 1),
} WorkerEventFlags;
bool js_delay_with_flags(struct mjs* mjs, uint32_t time);
void js_flags_set(struct mjs* mjs, uint32_t flags);
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout);
#ifdef __cplusplus
}
#endif

View File

@@ -1,291 +0,0 @@
#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

@@ -1,212 +0,0 @@
#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

@@ -1,529 +0,0 @@
#include <core/common_defines.h>
#include "../js_modules.h"
#include <furi_hal.h>
#define ASCII_TO_KEY(layout, x) (((uint8_t)x < 128) ? (layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
typedef struct {
FuriHalUsbHidConfig* hid_cfg;
uint16_t layout[128];
FuriHalUsbInterface* usb_if_prev;
uint8_t key_hold_cnt;
} JsBadusbInst;
static const struct {
char* name;
uint16_t code;
} key_codes[] = {
{"CTRL", KEY_MOD_LEFT_CTRL},
{"SHIFT", KEY_MOD_LEFT_SHIFT},
{"ALT", KEY_MOD_LEFT_ALT},
{"GUI", KEY_MOD_LEFT_GUI},
{"DOWN", HID_KEYBOARD_DOWN_ARROW},
{"LEFT", HID_KEYBOARD_LEFT_ARROW},
{"RIGHT", HID_KEYBOARD_RIGHT_ARROW},
{"UP", HID_KEYBOARD_UP_ARROW},
{"ENTER", HID_KEYBOARD_RETURN},
{"PAUSE", HID_KEYBOARD_PAUSE},
{"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
{"DELETE", HID_KEYBOARD_DELETE_FORWARD},
{"BACKSPACE", HID_KEYBOARD_DELETE},
{"END", HID_KEYBOARD_END},
{"ESC", HID_KEYBOARD_ESCAPE},
{"HOME", HID_KEYBOARD_HOME},
{"INSERT", HID_KEYBOARD_INSERT},
{"NUMLOCK", HID_KEYPAD_NUMLOCK},
{"PAGEUP", HID_KEYBOARD_PAGE_UP},
{"PAGEDOWN", HID_KEYBOARD_PAGE_DOWN},
{"PRINTSCREEN", HID_KEYBOARD_PRINT_SCREEN},
{"SCROLLLOCK", HID_KEYBOARD_SCROLL_LOCK},
{"SPACE", HID_KEYBOARD_SPACEBAR},
{"TAB", HID_KEYBOARD_TAB},
{"MENU", HID_KEYBOARD_APPLICATION},
{"F1", HID_KEYBOARD_F1},
{"F2", HID_KEYBOARD_F2},
{"F3", HID_KEYBOARD_F3},
{"F4", HID_KEYBOARD_F4},
{"F5", HID_KEYBOARD_F5},
{"F6", HID_KEYBOARD_F6},
{"F7", HID_KEYBOARD_F7},
{"F8", HID_KEYBOARD_F8},
{"F9", HID_KEYBOARD_F9},
{"F10", HID_KEYBOARD_F10},
{"F11", HID_KEYBOARD_F11},
{"F12", HID_KEYBOARD_F12},
{"F13", HID_KEYBOARD_F13},
{"F14", HID_KEYBOARD_F14},
{"F15", HID_KEYBOARD_F15},
{"F16", HID_KEYBOARD_F16},
{"F17", HID_KEYBOARD_F17},
{"F18", HID_KEYBOARD_F18},
{"F19", HID_KEYBOARD_F19},
{"F20", HID_KEYBOARD_F20},
{"F21", HID_KEYBOARD_F21},
{"F22", HID_KEYBOARD_F22},
{"F23", HID_KEYBOARD_F23},
{"F24", HID_KEYBOARD_F24},
{"NUM0", HID_KEYPAD_0},
{"NUM1", HID_KEYPAD_1},
{"NUM2", HID_KEYPAD_2},
{"NUM3", HID_KEYPAD_3},
{"NUM4", HID_KEYPAD_4},
{"NUM5", HID_KEYPAD_5},
{"NUM6", HID_KEYPAD_6},
{"NUM7", HID_KEYPAD_7},
{"NUM8", HID_KEYPAD_8},
{"NUM9", HID_KEYPAD_9},
};
static void js_badusb_quit_free(JsBadusbInst* badusb) {
if(badusb->usb_if_prev) {
furi_hal_hid_kb_release_all();
furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL));
badusb->usb_if_prev = NULL;
}
if(badusb->hid_cfg) {
free(badusb->hid_cfg);
badusb->hid_cfg = NULL;
}
}
static bool setup_parse_params(
JsBadusbInst* badusb,
struct mjs* mjs,
mjs_val_t arg,
FuriHalUsbHidConfig* hid_cfg) {
if(!mjs_is_object(arg)) {
return false;
}
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
mjs_val_t layout_obj = mjs_get(mjs, arg, "layoutPath", ~0);
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
hid_cfg->pid = mjs_get_int32(mjs, pid_obj);
} else {
return false;
}
if(mjs_is_string(mfr_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &mfr_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
strlcpy(hid_cfg->manuf, str_temp, sizeof(hid_cfg->manuf));
}
if(mjs_is_string(prod_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &prod_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product));
}
if(mjs_is_string(layout_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &layout_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
bool layout_loaded = storage_file_open(file, str_temp, FSAM_READ, FSOM_OPEN_EXISTING) &&
storage_file_read(file, badusb->layout, sizeof(badusb->layout)) ==
sizeof(badusb->layout);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(!layout_loaded) {
return false;
}
} else {
memcpy(badusb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(badusb->layout)));
}
return true;
}
static void js_badusb_setup(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is already started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
size_t num_args = mjs_nargs(mjs);
if(num_args == 0) {
// No arguments: start USB HID with default settings
args_correct = true;
} else if(num_args == 1) {
badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig));
// Parse argument object
args_correct = setup_parse_params(badusb, mjs, mjs_arg(mjs, 0), badusb->hid_cfg);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
badusb->usb_if_prev = furi_hal_usb_get_config();
furi_hal_usb_unlock();
furi_hal_usb_set_config(&usb_hid, badusb->hid_cfg);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_quit(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_badusb_quit_free(badusb);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_is_connected(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool is_connected = furi_hal_hid_is_connected();
mjs_return(mjs, mjs_mk_boolean(mjs, is_connected));
}
uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) {
if(name_len == 1) { // Single char
return (ASCII_TO_KEY(badusb->layout, key_name[0]));
}
for(size_t i = 0; i < COUNT_OF(key_codes); i++) {
size_t key_cmd_len = strlen(key_codes[i].name);
if(key_cmd_len != name_len) {
continue;
}
if(strncmp(key_name, key_codes[i].name, name_len) == 0) {
return key_codes[i].code;
}
}
return HID_KEYBOARD_NONE;
}
static bool parse_keycode(JsBadusbInst* badusb, struct mjs* mjs, size_t nargs, uint16_t* keycode) {
uint16_t key_tmp = 0;
for(size_t i = 0; i < nargs; i++) {
mjs_val_t arg = mjs_arg(mjs, i);
if(mjs_is_string(arg)) {
size_t name_len = 0;
const char* key_name = mjs_get_string(mjs, &arg, &name_len);
if((key_name == NULL) || (name_len == 0)) {
// String error
return false;
}
uint16_t str_key = get_keycode_by_name(badusb, key_name, name_len);
if(str_key == HID_KEYBOARD_NONE) {
// Unknown key code
return false;
}
if((str_key & 0xFF) && (key_tmp & 0xFF)) {
// Main key is already defined
return false;
}
key_tmp |= str_key;
} else if(mjs_is_number(arg)) {
uint32_t keycode_number = (uint32_t)mjs_get_int32(mjs, arg);
if(((key_tmp & 0xFF) != 0) || (keycode_number > 0xFF)) {
return false;
}
key_tmp |= keycode_number & 0xFF;
} else {
return false;
}
}
*keycode = key_tmp;
return true;
}
static void js_badusb_press(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args > 0) {
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_hold(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args > 0) {
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(keycode & 0xFF) {
badusb->key_hold_cnt++;
if(badusb->key_hold_cnt > (HID_KB_MAX_KEYS - 1)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Too many keys are hold");
furi_hal_hid_kb_release_all();
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
furi_hal_hid_kb_press(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_release(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args == 0) {
furi_hal_hid_kb_release_all();
badusb->key_hold_cnt = 0;
mjs_return(mjs, MJS_UNDEFINED);
return;
} else {
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if((keycode & 0xFF) && (badusb->key_hold_cnt > 0)) {
badusb->key_hold_cnt--;
}
furi_hal_hid_kb_release(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
// Make sure NUMLOCK is enabled for altchar
static void ducky_numlock_on() {
if((furi_hal_hid_get_led_state() & HID_KB_LED_NUM) == 0) {
furi_hal_hid_kb_press(HID_KEYBOARD_LOCK_NUM_LOCK);
furi_hal_hid_kb_release(HID_KEYBOARD_LOCK_NUM_LOCK);
}
}
// Simulate pressing a character using ALT+Numpad ASCII code
static void ducky_altchar(JsBadusbInst* badusb, const char* ascii_code) {
// Hold the ALT key
furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT);
// Press the corresponding numpad key for each digit of the ASCII code
for(size_t i = 0; ascii_code[i] != '\0'; i++) {
char digitChar[5] = {'N', 'U', 'M', ascii_code[i], '\0'}; // Construct the numpad key name
uint16_t numpad_keycode = get_keycode_by_name(badusb, digitChar, strlen(digitChar));
if(numpad_keycode == HID_KEYBOARD_NONE) {
continue; // Skip if keycode not found
}
furi_hal_hid_kb_press(numpad_keycode);
furi_hal_hid_kb_release(numpad_keycode);
}
// Release the ALT key
furi_hal_hid_kb_release(KEY_MOD_LEFT_ALT);
}
static void badusb_print(struct mjs* mjs, bool ln, bool alt) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
const char* text_str = NULL;
size_t text_len = 0;
uint32_t delay_val = 0;
do {
mjs_val_t obj_string = MJS_UNDEFINED;
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
obj_string = mjs_arg(mjs, 0);
} else if(num_args == 2) {
obj_string = mjs_arg(mjs, 0);
mjs_val_t obj_delay = mjs_arg(mjs, 1);
if(!mjs_is_number(obj_delay)) {
break;
}
delay_val = (uint32_t)mjs_get_int32(mjs, obj_delay);
if(delay_val > 60000) {
break;
}
}
if(!mjs_is_string(obj_string)) {
break;
}
text_str = mjs_get_string(mjs, &obj_string, &text_len);
if((text_str == NULL) || (text_len == 0)) {
break;
}
args_correct = true;
} while(0);
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(alt) {
ducky_numlock_on();
}
for(size_t i = 0; i < text_len; i++) {
if(alt) {
// Convert character to ascii numeric value
char ascii_str[4];
snprintf(ascii_str, sizeof(ascii_str), "%u", (uint8_t)text_str[i]);
ducky_altchar(badusb, ascii_str);
} else {
uint16_t keycode = ASCII_TO_KEY(badusb->layout, text_str[i]);
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
}
if(delay_val > 0) {
bool need_exit = js_delay_with_flags(mjs, delay_val);
if(need_exit) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
}
if(ln) {
furi_hal_hid_kb_press(HID_KEYBOARD_RETURN);
furi_hal_hid_kb_release(HID_KEYBOARD_RETURN);
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_print(struct mjs* mjs) {
badusb_print(mjs, false, false);
}
static void js_badusb_println(struct mjs* mjs) {
badusb_print(mjs, true, false);
}
static void js_badusb_alt_print(struct mjs* mjs) {
badusb_print(mjs, false, true);
}
static void js_badusb_alt_println(struct mjs* mjs) {
badusb_print(mjs, true, true);
}
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
mjs_val_t badusb_obj = mjs_mk_object(mjs);
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
mjs_set(mjs, badusb_obj, "setup", ~0, MJS_MK_FN(js_badusb_setup));
mjs_set(mjs, badusb_obj, "quit", ~0, MJS_MK_FN(js_badusb_quit));
mjs_set(mjs, badusb_obj, "isConnected", ~0, MJS_MK_FN(js_badusb_is_connected));
mjs_set(mjs, badusb_obj, "press", ~0, MJS_MK_FN(js_badusb_press));
mjs_set(mjs, badusb_obj, "hold", ~0, MJS_MK_FN(js_badusb_hold));
mjs_set(mjs, badusb_obj, "release", ~0, MJS_MK_FN(js_badusb_release));
mjs_set(mjs, badusb_obj, "print", ~0, MJS_MK_FN(js_badusb_print));
mjs_set(mjs, badusb_obj, "println", ~0, MJS_MK_FN(js_badusb_println));
mjs_set(mjs, badusb_obj, "altPrint", ~0, MJS_MK_FN(js_badusb_alt_print));
mjs_set(mjs, badusb_obj, "altPrintln", ~0, MJS_MK_FN(js_badusb_alt_println));
*object = badusb_obj;
return badusb;
}
static void js_badusb_destroy(void* inst) {
JsBadusbInst* badusb = inst;
js_badusb_quit_free(badusb);
free(badusb);
}
static const JsModuleDescriptor js_badusb_desc = {
"badusb",
js_badusb_create,
js_badusb_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_badusb_desc,
};
const FlipperAppPluginDescriptor* js_badusb_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,246 +0,0 @@
#include "../js_modules.h"
#include <furi_hal_bt.h>
#include <extra_beacon.h>
typedef struct {
bool saved_prev_cfg;
bool prev_cfg_set;
GapExtraBeaconConfig prev_cfg;
bool saved_prev_data;
uint8_t prev_data[EXTRA_BEACON_MAX_DATA_SIZE];
uint8_t prev_data_len;
bool saved_prev_active;
bool prev_active;
bool keep_alive;
} JsBlebeaconInst;
static JsBlebeaconInst* get_this_ctx(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBlebeaconInst* blebeacon = mjs_get_ptr(mjs, obj_inst);
furi_assert(blebeacon);
return blebeacon;
}
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 void ret_int_err(struct mjs* mjs, const char* error) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", error);
mjs_return(mjs, MJS_UNDEFINED);
}
static bool check_arg_count(struct mjs* mjs, size_t count) {
size_t num_args = mjs_nargs(mjs);
if(num_args != count) {
ret_bad_args(mjs, "Wrong argument count");
return false;
}
return true;
}
static void js_blebeacon_is_active(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 0)) return;
UNUSED(blebeacon);
mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_bt_extra_beacon_is_active()));
}
static void js_blebeacon_set_config(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(mjs_nargs(mjs) < 1 || mjs_nargs(mjs) > 4) {
ret_bad_args(mjs, "Wrong argument count");
return;
}
char* mac = NULL;
size_t mac_len = 0;
mjs_val_t mac_arg = mjs_arg(mjs, 0);
if(mjs_is_typed_array(mac_arg)) {
if(mjs_is_data_view(mac_arg)) {
mac_arg = mjs_dataview_get_buf(mjs, mac_arg);
}
mac = mjs_array_buf_get_ptr(mjs, mac_arg, &mac_len);
}
if(!mac || mac_len != EXTRA_BEACON_MAC_ADDR_SIZE) {
ret_bad_args(mjs, "Wrong MAC address");
return;
}
uint8_t power = GapAdvPowerLevel_0dBm;
mjs_val_t power_arg = mjs_arg(mjs, 1);
if(mjs_is_number(power_arg)) {
power = mjs_get_int32(mjs, power_arg);
}
power = CLAMP(power, GapAdvPowerLevel_6dBm, GapAdvPowerLevel_Neg40dBm);
uint8_t intv_min = 50;
mjs_val_t intv_min_arg = mjs_arg(mjs, 2);
if(mjs_is_number(intv_min_arg)) {
intv_min = mjs_get_int32(mjs, intv_min_arg);
}
intv_min = MAX(intv_min, 20);
uint8_t intv_max = 150;
mjs_val_t intv_max_arg = mjs_arg(mjs, 3);
if(mjs_is_number(intv_max_arg)) {
intv_max = mjs_get_int32(mjs, intv_max_arg);
}
intv_max = MAX(intv_max, intv_min);
GapExtraBeaconConfig config = {
.min_adv_interval_ms = intv_min,
.max_adv_interval_ms = intv_max,
.adv_channel_map = GapAdvChannelMapAll,
.adv_power_level = power,
.address_type = GapAddressTypePublic,
};
memcpy(config.address, (uint8_t*)mac, sizeof(config.address));
if(!blebeacon->saved_prev_cfg) {
blebeacon->saved_prev_cfg = true;
const GapExtraBeaconConfig* prev_cfg_ptr = furi_hal_bt_extra_beacon_get_config();
if(prev_cfg_ptr) {
blebeacon->prev_cfg_set = true;
memcpy(&blebeacon->prev_cfg, prev_cfg_ptr, sizeof(blebeacon->prev_cfg));
} else {
blebeacon->prev_cfg_set = false;
}
}
if(!furi_hal_bt_extra_beacon_set_config(&config)) {
ret_int_err(mjs, "Failed setting beacon config");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_set_data(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 1)) return;
char* data = NULL;
size_t data_len = 0;
mjs_val_t data_arg = mjs_arg(mjs, 0);
if(mjs_is_typed_array(data_arg)) {
if(mjs_is_data_view(data_arg)) {
data_arg = mjs_dataview_get_buf(mjs, data_arg);
}
data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len);
}
if(!data) {
ret_bad_args(mjs, "Data must be a Uint8Array");
return;
}
if(!blebeacon->saved_prev_data) {
blebeacon->saved_prev_data = true;
blebeacon->prev_data_len = furi_hal_bt_extra_beacon_get_data(blebeacon->prev_data);
}
if(!furi_hal_bt_extra_beacon_set_data((uint8_t*)data, data_len)) {
ret_int_err(mjs, "Failed setting beacon data");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_start(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 0)) return;
if(!blebeacon->saved_prev_active) {
blebeacon->saved_prev_active = true;
blebeacon->prev_active = furi_hal_bt_extra_beacon_is_active();
}
if(!furi_hal_bt_extra_beacon_start()) {
ret_int_err(mjs, "Failed starting beacon");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_stop(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 0)) return;
UNUSED(blebeacon);
if(!blebeacon->saved_prev_active) {
blebeacon->saved_prev_active = true;
blebeacon->prev_active = furi_hal_bt_extra_beacon_is_active();
}
if(!furi_hal_bt_extra_beacon_stop()) {
ret_int_err(mjs, "Failed stopping beacon");
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_blebeacon_keep_alive(struct mjs* mjs) {
JsBlebeaconInst* blebeacon = get_this_ctx(mjs);
if(!check_arg_count(mjs, 1)) return;
mjs_val_t bool_obj = mjs_arg(mjs, 0);
blebeacon->keep_alive = mjs_get_bool(mjs, bool_obj);
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_blebeacon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsBlebeaconInst* blebeacon = malloc(sizeof(JsBlebeaconInst));
mjs_val_t blebeacon_obj = mjs_mk_object(mjs);
mjs_set(mjs, blebeacon_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, blebeacon));
mjs_set(mjs, blebeacon_obj, "isActive", ~0, MJS_MK_FN(js_blebeacon_is_active));
mjs_set(mjs, blebeacon_obj, "setConfig", ~0, MJS_MK_FN(js_blebeacon_set_config));
mjs_set(mjs, blebeacon_obj, "setData", ~0, MJS_MK_FN(js_blebeacon_set_data));
mjs_set(mjs, blebeacon_obj, "start", ~0, MJS_MK_FN(js_blebeacon_start));
mjs_set(mjs, blebeacon_obj, "stop", ~0, MJS_MK_FN(js_blebeacon_stop));
mjs_set(mjs, blebeacon_obj, "keepAlive", ~0, MJS_MK_FN(js_blebeacon_keep_alive));
*object = blebeacon_obj;
return blebeacon;
}
static void js_blebeacon_destroy(void* inst) {
JsBlebeaconInst* blebeacon = inst;
if(!blebeacon->keep_alive) {
if(furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_stop());
}
if(blebeacon->saved_prev_cfg && blebeacon->prev_cfg_set) {
furi_check(furi_hal_bt_extra_beacon_set_config(&blebeacon->prev_cfg));
}
if(blebeacon->saved_prev_data) {
furi_check(
furi_hal_bt_extra_beacon_set_data(blebeacon->prev_data, blebeacon->prev_data_len));
}
if(blebeacon->prev_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
}
free(blebeacon);
}
static const JsModuleDescriptor js_blebeacon_desc = {
"blebeacon",
js_blebeacon_create,
js_blebeacon_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_blebeacon_desc,
};
const FlipperAppPluginDescriptor* js_blebeacon_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,458 +0,0 @@
#include "js_event_loop.h"
#include "../../js_modules.h" // IWYU pragma: keep
#include <expansion/expansion.h>
#include <mlib/m-array.h>
/**
* @brief Number of arguments that callbacks receive from this module that they can't modify
*/
#define SYSTEM_ARGS 2
/**
* @brief Context passed to the generic event callback
*/
typedef struct {
FuriEventLoop* event_loop;
JsEventLoopObjectType object_type;
struct mjs* mjs;
mjs_val_t callback;
// NOTE: not using an mlib array because resizing is not needed.
mjs_val_t* arguments;
size_t arity;
JsEventLoopTransformer transformer;
void* transformer_context;
} JsEventLoopCallbackContext;
/**
* @brief Contains data needed to cancel a subscription
*/
typedef struct {
FuriEventLoop* loop;
JsEventLoopObjectType object_type;
FuriEventLoopObject* object;
JsEventLoopCallbackContext* context;
JsEventLoopContract* contract;
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
} JsEventLoopSubscription;
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
/**
* @brief Per-module instance control structure
*/
struct JsEventLoop {
FuriEventLoop* loop;
SubscriptionArray_t subscriptions;
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
};
/**
* @brief Generic event callback, handles all events by calling the JS callbacks
*/
static void js_event_loop_callback_generic(void* param) {
JsEventLoopCallbackContext* context = param;
mjs_val_t result;
mjs_err_t error = mjs_apply(
context->mjs,
&result,
context->callback,
MJS_UNDEFINED,
context->arity,
context->arguments);
bool is_error = strcmp(mjs_strerror(context->mjs, error), "NO_ERROR") != 0;
bool asked_to_stop = js_flags_wait(context->mjs, ThreadEventStop, 0) & ThreadEventStop;
if(is_error || asked_to_stop) {
furi_event_loop_stop(context->event_loop);
}
// save returned args for next call
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i);
mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
}
}
/**
* @brief Handles non-timer events
*/
static void js_event_loop_callback(void* object, void* param) {
JsEventLoopCallbackContext* context = param;
if(context->transformer) {
mjs_disown(context->mjs, &context->arguments[1]);
context->arguments[1] =
context->transformer(context->mjs, object, context->transformer_context);
mjs_own(context->mjs, &context->arguments[1]);
} else {
// default behavior: take semaphores and mutexes
switch(context->object_type) {
case JsEventLoopObjectTypeSemaphore: {
FuriSemaphore* semaphore = object;
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
} break;
default:
// the corresponding check has been performed when we were given the contract
furi_crash();
}
}
js_event_loop_callback_generic(param);
}
/**
* @brief Cancels an event subscription
*/
static void js_event_loop_subscription_cancel(struct mjs* mjs) {
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
// timer operations are deferred, which creates lifetime issues
// just stop the timer and let the cleanup routine free everything when the script is done
furi_event_loop_timer_stop(subscription->object);
return;
}
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
free(subscription->context->arguments);
free(subscription->context);
// find and remove ourselves from the array
SubscriptionArray_it_t iterator;
for(SubscriptionArray_it(iterator, subscription->subscriptions);
!SubscriptionArray_end_p(iterator);
SubscriptionArray_next(iterator)) {
JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator);
if(item == subscription) break;
}
SubscriptionArray_remove(subscription->subscriptions, iterator);
free(subscription);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief Subscribes a JavaScript function to an event
*/
static void js_event_loop_subscribe(struct mjs* mjs) {
JsEventLoop* module = JS_GET_CONTEXT(mjs);
// get arguments
static const JsValueDeclaration js_loop_subscribe_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeRawPointer),
JS_VALUE_SIMPLE(JsValueTypeFunction),
};
static const JsValueArguments js_loop_subscribe_args =
JS_VALUE_ARGS(js_loop_subscribe_arg_list);
JsEventLoopContract* contract;
mjs_val_t callback;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_subscribe_args, &contract, &callback);
// create subscription object
JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription));
JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext));
subscription->loop = module->loop;
subscription->object_type = contract->object_type;
subscription->context = context;
subscription->subscriptions = module->subscriptions;
if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract;
mjs_val_t subscription_obj = mjs_mk_object(mjs);
mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription));
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
// create callback context
context->event_loop = module->loop;
context->object_type = contract->object_type;
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
context->arguments[0] = subscription_obj;
context->arguments[1] = MJS_UNDEFINED;
for(size_t i = SYSTEM_ARGS; i < context->arity; i++) {
mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2);
context->arguments[i] = arg;
mjs_own(mjs, &context->arguments[i]);
}
context->mjs = mjs;
context->callback = callback;
mjs_own(mjs, &context->callback);
mjs_own(mjs, &context->arguments[0]);
mjs_own(mjs, &context->arguments[1]);
// queue and stream contracts must have a transform callback, others are allowed to delegate
// the obvious default behavior to this module
if(contract->object_type == JsEventLoopObjectTypeQueue ||
contract->object_type == JsEventLoopObjectTypeStream) {
furi_check(contract->non_timer.transformer);
}
context->transformer = contract->non_timer.transformer;
context->transformer_context = contract->non_timer.transformer_context;
// subscribe
switch(contract->object_type) {
case JsEventLoopObjectTypeTimer: {
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
module->loop, js_event_loop_callback_generic, contract->timer.type, context);
furi_event_loop_timer_start(timer, contract->timer.interval_ticks);
contract->object = timer;
} break;
case JsEventLoopObjectTypeSemaphore:
furi_event_loop_subscribe_semaphore(
module->loop,
contract->object,
contract->non_timer.event,
js_event_loop_callback,
context);
break;
case JsEventLoopObjectTypeQueue:
furi_event_loop_subscribe_message_queue(
module->loop,
contract->object,
contract->non_timer.event,
js_event_loop_callback,
context);
break;
default:
furi_crash("unimplemented");
}
subscription->object = contract->object;
SubscriptionArray_push_back(module->subscriptions, subscription);
mjs_return(mjs, subscription_obj);
}
/**
* @brief Runs the event loop until it is stopped
*/
static void js_event_loop_run(struct mjs* mjs) {
JsEventLoop* module = JS_GET_CONTEXT(mjs);
furi_event_loop_run(module->loop);
}
/**
* @brief Stops a running event loop
*/
static void js_event_loop_stop(struct mjs* mjs) {
JsEventLoop* module = JS_GET_CONTEXT(mjs);
furi_event_loop_stop(module->loop);
}
/**
* @brief Creates a timer event that can be subscribed to just like any other
* event
*/
static void js_event_loop_timer(struct mjs* mjs) {
static const JsValueEnumVariant js_loop_timer_mode_variants[] = {
{"periodic", FuriEventLoopTimerTypePeriodic},
{"oneshot", FuriEventLoopTimerTypeOnce},
};
static const JsValueDeclaration js_loop_timer_arg_list[] = {
JS_VALUE_ENUM(FuriEventLoopTimerType, js_loop_timer_mode_variants),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_loop_timer_args = JS_VALUE_ARGS(js_loop_timer_arg_list);
FuriEventLoopTimerType mode;
int32_t interval;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_timer_args, &mode, &interval);
JsEventLoop* module = JS_GET_CONTEXT(mjs);
// make timer contract
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
*contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeTimer,
.object = NULL,
.timer =
{
.interval_ticks = furi_ms_to_ticks((uint32_t)interval),
.type = mode,
},
};
ContractArray_push_back(module->owned_contracts, contract);
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
}
/**
* @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and
* returns their dereferenced value
*/
static mjs_val_t
js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
UNUSED(context);
mjs_val_t* message_ptr;
furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk);
mjs_val_t message = *message_ptr;
mjs_disown(mjs, message_ptr);
free(message_ptr);
return message;
}
/**
* @brief Sends a message to a queue
*/
static void js_event_loop_queue_send(struct mjs* mjs) {
// get arguments
static const JsValueDeclaration js_loop_q_send_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_loop_q_send_args = JS_VALUE_ARGS(js_loop_q_send_arg_list);
mjs_val_t message;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_send_args, &message);
JsEventLoopContract* contract = JS_GET_CONTEXT(mjs);
// send message
mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t));
*message_ptr = message;
mjs_own(mjs, message_ptr);
furi_message_queue_put(contract->object, &message_ptr, 0);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief Creates a queue
*/
static void js_event_loop_queue(struct mjs* mjs) {
// get arguments
static const JsValueDeclaration js_loop_q_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_loop_q_args = JS_VALUE_ARGS(js_loop_q_arg_list);
int32_t length;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_args, &length);
JsEventLoop* module = JS_GET_CONTEXT(mjs);
// make queue contract
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
*contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
// we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values
.object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)),
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = js_event_loop_queue_transformer,
},
};
ContractArray_push_back(module->owned_contracts, contract);
// return object with control methods
mjs_val_t queue = mjs_mk_object(mjs);
mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract));
mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract));
mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send));
mjs_return(mjs, queue);
}
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t event_loop_obj = mjs_mk_object(mjs);
JsEventLoop* module = malloc(sizeof(JsEventLoop));
module->loop = furi_event_loop_alloc();
SubscriptionArray_init(module->subscriptions);
ContractArray_init(module->owned_contracts);
JS_ASSIGN_MULTI(mjs, event_loop_obj) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
JS_FIELD("subscribe", MJS_MK_FN(js_event_loop_subscribe));
JS_FIELD("run", MJS_MK_FN(js_event_loop_run));
JS_FIELD("stop", MJS_MK_FN(js_event_loop_stop));
JS_FIELD("timer", MJS_MK_FN(js_event_loop_timer));
JS_FIELD("queue", MJS_MK_FN(js_event_loop_queue));
}
*object = event_loop_obj;
return module;
}
static void js_event_loop_destroy(void* inst) {
if(inst) {
JsEventLoop* module = inst;
furi_event_loop_stop(module->loop);
// free subscriptions
SubscriptionArray_it_t sub_iterator;
for(SubscriptionArray_it(sub_iterator, module->subscriptions);
!SubscriptionArray_end_p(sub_iterator);
SubscriptionArray_next(sub_iterator)) {
JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator);
free((*sub)->context->arguments);
free((*sub)->context);
free(*sub);
}
SubscriptionArray_clear(module->subscriptions);
// free owned contracts
ContractArray_it_t iterator;
for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator);
ContractArray_next(iterator)) {
// unsubscribe object
JsEventLoopContract* contract = *ContractArray_cref(iterator);
if(contract->object_type == JsEventLoopObjectTypeTimer) {
furi_event_loop_timer_stop(contract->object);
} else {
furi_event_loop_unsubscribe(module->loop, contract->object);
}
// free object
switch(contract->object_type) {
case JsEventLoopObjectTypeTimer:
furi_event_loop_timer_free(contract->object);
break;
case JsEventLoopObjectTypeSemaphore:
furi_semaphore_free(contract->object);
break;
case JsEventLoopObjectTypeQueue:
furi_message_queue_free(contract->object);
break;
default:
furi_crash("unimplemented");
}
free(contract);
}
ContractArray_clear(module->owned_contracts);
furi_event_loop_free(module->loop);
free(module);
}
}
extern const ElfApiInterface js_event_loop_hashtable_api_interface;
static const JsModuleDescriptor js_event_loop_desc = {
"event_loop",
js_event_loop_create,
js_event_loop_destroy,
&js_event_loop_hashtable_api_interface,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_event_loop_desc,
};
const FlipperAppPluginDescriptor* js_event_loop_ep(void) {
return &plugin_descriptor;
}
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) {
// porta: not the proudest function that i ever wrote
furi_check(loop);
return loop->loop;
}

View File

@@ -1,104 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include <furi/core/event_loop.h>
#include <furi/core/event_loop_timer.h>
/**
* @file js_event_loop.h
*
* In JS interpreter code, `js_event_loop` always creates and maintains the
* event loop. There are two ways in which other modules can integrate with this
* loop:
* - Via contracts: The user of your module would have to acquire an opaque
* JS value from you and pass it to `js_event_loop`. This is useful for
* events that they user may be interested in. For more info, look at
* `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which
* you will need to unsubscribe the event loop from your object.
* - Directly: When your module is created, you can acquire an instance of
* `JsEventLoop` which you can use to acquire an instance of
* `FuriEventLoop` that you can manipulate directly, without the JS
* programmer having to pass contracts around. This is useful for
* "behind-the-scenes" events that the user does not need to know about. For
* more info, look at `js_event_loop_get_loop`.
*
* In both cases, your module is responsible for both instantiating,
* unsubscribing and freeing the object that the event loop subscribes to.
*/
#ifdef __cplusplus
extern "C" {
#endif
typedef struct JsEventLoop JsEventLoop;
typedef enum {
JsEventLoopObjectTypeTimer,
JsEventLoopObjectTypeQueue,
JsEventLoopObjectTypeMutex,
JsEventLoopObjectTypeSemaphore,
JsEventLoopObjectTypeStream,
} JsEventLoopObjectType;
typedef mjs_val_t (
*JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context);
typedef struct {
FuriEventLoopEvent event;
JsEventLoopTransformer transformer;
void* transformer_context;
} JsEventLoopNonTimerContract;
typedef struct {
FuriEventLoopTimerType type;
uint32_t interval_ticks;
} JsEventLoopTimerContract;
/**
* @brief Adapter for other JS modules that wish to integrate with the event
* loop JS module
*
* If another module wishes to integrate with `js_event_loop`, it needs to
* implement a function callable from JS that returns an mJS foreign pointer to
* an instance of this structure. This value is then read by `event_loop`'s
* `subscribe` function.
*
* There are two fundamental variants of this structure:
* - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is
* valid, and the `non_timer` field is invalid.
* - `object_type` is something else: the `timer` field is invalid, and the
* `non_timer` field is valid. `non_timer.event` will be passed to
* `furi_event_loop_subscribe`. `non_timer.transformer` will be called to
* transform an object into a JS value (called an item) that's passed to the
* JS callback. This is useful for example to take an item out of a message
* queue and pass it to JS code in a convenient format. If
* `non_timer.transformer` is NULL, the event loop will take semaphores and
* mutexes on its own.
*
* The producer of the contract is responsible for freeing both the contract and
* the object that it points to when the interpreter is torn down.
*/
typedef struct {
JsForeignMagic magic; // <! `JsForeignMagic_JsEventLoopContract`
JsEventLoopObjectType object_type;
FuriEventLoopObject* object;
union {
JsEventLoopNonTimerContract non_timer;
JsEventLoopTimerContract timer;
};
} JsEventLoopContract;
static_assert(offsetof(JsEventLoopContract, magic) == 0);
/**
* @brief Gets the FuriEventLoop owned by a JsEventLoop
*
* This function is useful in case your JS module wishes to integrate with
* the event loop without passing contracts through JS code. Your module will be
* dynamically linked to this one if you use this function, but only if JS code
* imports `event_loop` _before_ your module. An instance of `JsEventLoop` may
* be obtained via `js_module_get`.
*/
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop);
#ifdef __cplusplus
}
#endif

View File

@@ -1,16 +0,0 @@
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/api_hashtable/compilesort.hpp>
#include "js_event_loop_api_table_i.h"
static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!");
extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{
{
.api_version_major = 0,
.api_version_minor = 0,
.resolver_callback = &elf_resolve_from_hashtable,
},
js_event_loop_api_table.cbegin(),
js_event_loop_api_table.cend(),
};

View File

@@ -1,4 +0,0 @@
#include "js_event_loop.h"
static constexpr auto js_event_loop_api_table = sort(
create_array_t<sym_entry>(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*))));

View File

@@ -1,45 +0,0 @@
#include "../js_modules.h" // IWYU pragma: keep
#include <core/common_defines.h>
#include <furi_hal_version.h>
#include <power/power_service/power.h>
static void js_flipper_get_model(struct mjs* mjs) {
mjs_val_t ret = mjs_mk_string(mjs, furi_hal_version_get_model_name(), ~0, true);
mjs_return(mjs, ret);
}
static void js_flipper_get_name(struct mjs* mjs) {
const char* name_str = furi_hal_version_get_name_ptr();
if(name_str == NULL) {
name_str = "Unknown";
}
mjs_val_t ret = mjs_mk_string(mjs, name_str, ~0, true);
mjs_return(mjs, ret);
}
static void js_flipper_get_battery(struct mjs* mjs) {
Power* power = furi_record_open(RECORD_POWER);
PowerInfo info;
power_get_info(power, &info);
furi_record_close(RECORD_POWER);
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
}
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t sdk_vsn = mjs_mk_array(mjs);
mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MAJOR));
mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MINOR));
mjs_val_t flipper_obj = mjs_mk_object(mjs);
*object = flipper_obj;
JS_ASSIGN_MULTI(mjs, flipper_obj) {
JS_FIELD("getModel", MJS_MK_FN(js_flipper_get_model));
JS_FIELD("getName", MJS_MK_FN(js_flipper_get_name));
JS_FIELD("getBatteryCharge", MJS_MK_FN(js_flipper_get_battery));
JS_FIELD("firmwareVendor", mjs_mk_string(mjs, JS_SDK_VENDOR_FIRMWARE, ~0, false));
JS_FIELD("jsSdkVersion", sdk_vsn);
}
return (void*)1;
}

View File

@@ -1,4 +0,0 @@
#pragma once
#include "../js_thread_i.h"
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);

View File

@@ -1,490 +0,0 @@
#include "../js_modules.h" // IWYU pragma: keep
#include "./js_event_loop/js_event_loop.h"
#include <furi_hal_gpio.h>
#include <furi_hal_pwm.h>
#include <furi_hal_resources.h>
#include <expansion/expansion.h>
#include <limits.h>
#include <mlib/m-array.h>
#define INTERRUPT_QUEUE_LEN 16
/**
* Per-pin control structure
*/
typedef struct {
const GpioPin* pin;
bool had_interrupt;
FuriSemaphore* interrupt_semaphore;
JsEventLoopContract* interrupt_contract;
FuriHalAdcChannel adc_channel;
FuriHalPwmOutputId pwm_output;
FuriHalAdcHandle* adc_handle;
} JsGpioPinInst;
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
#define M_OPL_ManagedPinsArray_t() ARRAY_OPLIST(ManagedPinsArray)
/**
* Per-module instance control structure
*/
typedef struct {
FuriEventLoop* loop;
ManagedPinsArray_t managed_pins;
FuriHalAdcHandle* adc_handle;
} JsGpioInst;
/**
* @brief Interrupt callback
*/
static void js_gpio_int_cb(void* arg) {
furi_assert(arg);
FuriSemaphore* semaphore = arg;
furi_semaphore_release(semaphore);
}
/**
* @brief Initializes a GPIO pin according to the provided mode object
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let led = gpio.get("pc3");
* led.init({ direction: "out", outMode: "push_pull" });
* ```
*/
static void js_gpio_init(struct mjs* mjs) {
// direction variants
typedef enum {
JsGpioDirectionIn,
JsGpioDirectionOut,
} JsGpioDirection;
static const JsValueEnumVariant js_gpio_direction_variants[] = {
{"in", JsGpioDirectionIn},
{"out", JsGpioDirectionOut},
};
static const JsValueDeclaration js_gpio_direction =
JS_VALUE_ENUM(JsGpioDirection, js_gpio_direction_variants);
// inMode variants
typedef enum {
JsGpioInModeAnalog = (0 << 0),
JsGpioInModePlainDigital = (1 << 0),
JsGpioInModeInterrupt = (2 << 0),
JsGpioInModeEvent = (3 << 0),
} JsGpioInMode;
static const JsValueEnumVariant js_gpio_in_mode_variants[] = {
{"analog", JsGpioInModeAnalog},
{"plain_digital", JsGpioInModePlainDigital},
{"interrupt", JsGpioInModeInterrupt},
{"event", JsGpioInModeEvent},
};
static const JsValueDeclaration js_gpio_in_mode =
JS_VALUE_ENUM_W_DEFAULT(JsGpioInMode, js_gpio_in_mode_variants, JsGpioInModePlainDigital);
// outMode variants
typedef enum {
JsGpioOutModePushPull,
JsGpioOutModeOpenDrain,
} JsGpioOutMode;
static const JsValueEnumVariant js_gpio_out_mode_variants[] = {
{"push_pull", JsGpioOutModePushPull},
{"open_drain", JsGpioOutModeOpenDrain},
};
static const JsValueDeclaration js_gpio_out_mode =
JS_VALUE_ENUM_W_DEFAULT(JsGpioOutMode, js_gpio_out_mode_variants, JsGpioOutModeOpenDrain);
// edge variants
typedef enum {
JsGpioEdgeRising = (0 << 2),
JsGpioEdgeFalling = (1 << 2),
JsGpioEdgeBoth = (2 << 2),
} JsGpioEdge;
static const JsValueEnumVariant js_gpio_edge_variants[] = {
{"rising", JsGpioEdgeRising},
{"falling", JsGpioEdgeFalling},
{"both", JsGpioEdgeBoth},
};
static const JsValueDeclaration js_gpio_edge =
JS_VALUE_ENUM_W_DEFAULT(JsGpioEdge, js_gpio_edge_variants, JsGpioEdgeRising);
// pull variants
static const JsValueEnumVariant js_gpio_pull_variants[] = {
{"up", GpioPullUp},
{"down", GpioPullDown},
};
static const JsValueDeclaration js_gpio_pull =
JS_VALUE_ENUM_W_DEFAULT(GpioPull, js_gpio_pull_variants, GpioPullNo);
// complete mode object
static const JsValueObjectField js_gpio_mode_object_fields[] = {
{"direction", &js_gpio_direction},
{"inMode", &js_gpio_in_mode},
{"outMode", &js_gpio_out_mode},
{"edge", &js_gpio_edge},
{"pull", &js_gpio_pull},
};
// function args
static const JsValueDeclaration js_gpio_init_arg_list[] = {
JS_VALUE_OBJECT_W_DEFAULTS(js_gpio_mode_object_fields),
};
static const JsValueArguments js_gpio_init_args = JS_VALUE_ARGS(js_gpio_init_arg_list);
JsGpioDirection direction;
JsGpioInMode in_mode;
JsGpioOutMode out_mode;
JsGpioEdge edge;
GpioPull pull;
JS_VALUE_PARSE_ARGS_OR_RETURN(
mjs, &js_gpio_init_args, &direction, &in_mode, &out_mode, &edge, &pull);
GpioMode mode;
if(direction == JsGpioDirectionOut) {
static const GpioMode js_gpio_out_mode_lut[] = {
[JsGpioOutModePushPull] = GpioModeOutputPushPull,
[JsGpioOutModeOpenDrain] = GpioModeOutputOpenDrain,
};
mode = js_gpio_out_mode_lut[out_mode];
} else {
static const GpioMode js_gpio_in_mode_lut[] = {
[JsGpioInModeAnalog] = GpioModeAnalog,
[JsGpioInModePlainDigital] = GpioModeInput,
[JsGpioInModeInterrupt | JsGpioEdgeRising] = GpioModeInterruptRise,
[JsGpioInModeInterrupt | JsGpioEdgeFalling] = GpioModeInterruptFall,
[JsGpioInModeInterrupt | JsGpioEdgeBoth] = GpioModeInterruptRiseFall,
[JsGpioInModeEvent | JsGpioEdgeRising] = GpioModeEventRise,
[JsGpioInModeEvent | JsGpioEdgeFalling] = GpioModeEventFall,
[JsGpioInModeEvent | JsGpioEdgeBoth] = GpioModeEventRiseFall,
};
mode = js_gpio_in_mode_lut[in_mode | edge];
}
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
furi_hal_gpio_init(manager_data->pin, mode, pull, GpioSpeedVeryHigh);
}
/**
* @brief Writes a logic value to a GPIO pin
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let led = gpio.get("pc3");
* led.init({ direction: "out", outMode: "push_pull" });
* led.write(true);
* ```
*/
static void js_gpio_write(struct mjs* mjs) {
static const JsValueDeclaration js_gpio_write_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeBool),
};
static const JsValueArguments js_gpio_write_args = JS_VALUE_ARGS(js_gpio_write_arg_list);
bool level;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_write_args, &level);
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
furi_hal_gpio_write(manager_data->pin, level);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief Reads a logic value from a GPIO pin
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let button = gpio.get("pc1");
* button.init({ direction: "in" });
* if(button.read())
* print("hi button!!!!!");
* ```
*/
static void js_gpio_read(struct mjs* mjs) {
// get level
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
bool value = furi_hal_gpio_read(manager_data->pin);
mjs_return(mjs, mjs_mk_boolean(mjs, value));
}
/**
* @brief Returns a event loop contract that can be used to listen to interrupts
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let button = gpio.get("pc1");
* let event_loop = require("event_loop");
* button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
* event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); });
* event_loop.run();
* ```
*/
static void js_gpio_interrupt(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
// interrupt handling
if(!manager_data->had_interrupt) {
furi_hal_gpio_add_int_callback(
manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore);
furi_hal_gpio_enable_int_callback(manager_data->pin);
manager_data->had_interrupt = true;
}
// make contract
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
*contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = manager_data->interrupt_semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
manager_data->interrupt_contract = contract;
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
}
/**
* @brief Reads a voltage from a GPIO pin in analog mode
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pot = gpio.get("pc0");
* pot.init({ direction: "in", inMode: "analog" });
* print("voltage:" pot.readAnalog(), "mV");
* ```
*/
static void js_gpio_read_analog(struct mjs* mjs) {
// get mV (ADC is configured for 12 bits and 2048 mV max)
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
uint16_t millivolts =
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
}
/**
* @brief Determines whether this pin supports PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* assert_eq(true, gpio.get("pa4").isPwmSupported());
* assert_eq(false, gpio.get("pa5").isPwmSupported());
* ```
*/
static void js_gpio_is_pwm_supported(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, manager_data->pwm_output != FuriHalPwmOutputIdNone));
}
/**
* @brief Sets PWM parameters and starts the PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pa4 = gpio.get("pa4");
* pa4.pwmWrite(10000, 50);
* ```
*/
static void js_gpio_pwm_write(struct mjs* mjs) {
static const JsValueDeclaration js_gpio_pwm_write_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_gpio_pwm_write_args =
JS_VALUE_ARGS(js_gpio_pwm_write_arg_list);
int32_t frequency, duty;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_pwm_write_args, &frequency, &duty);
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
if(furi_hal_pwm_is_running(manager_data->pwm_output)) {
furi_hal_pwm_set_params(manager_data->pwm_output, frequency, duty);
} else {
furi_hal_pwm_start(manager_data->pwm_output, frequency, duty);
}
}
/**
* @brief Determines whether PWM is running
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* assert_eq(false, gpio.get("pa4").isPwmRunning());
* ```
*/
static void js_gpio_is_pwm_running(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_pwm_is_running(manager_data->pwm_output)));
}
/**
* @brief Stops PWM
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let pa4 = gpio.get("pa4");
* pa4.pwmWrite(10000, 50);
* pa4.pwmStop();
* ```
*/
static void js_gpio_pwm_stop(struct mjs* mjs) {
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
}
furi_hal_pwm_stop(manager_data->pwm_output);
}
/**
* @brief Returns an object that manages a specified pin.
*
* Example usage:
*
* ```js
* let gpio = require("gpio");
* let led = gpio.get("pc3");
* ```
*/
static void js_gpio_get(struct mjs* mjs) {
static const JsValueDeclaration js_gpio_get_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_gpio_get_args = JS_VALUE_ARGS(js_gpio_get_arg_list);
mjs_val_t name_arg;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_get_args, &name_arg);
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
const GpioPinRecord* pin_record = NULL;
// parse input argument to a pin pointer
if(name_string) {
pin_record = furi_hal_resources_pin_by_name(name_string);
} else if(mjs_is_number(name_arg)) {
int name_int = mjs_get_int(mjs, name_arg);
pin_record = furi_hal_resources_pin_by_number(name_int);
} else {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
}
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
if(pin_record->debug)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
// return pin manager object
JsGpioInst* module = JS_GET_CONTEXT(mjs);
mjs_val_t manager = mjs_mk_object(mjs);
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
manager_data->pin = pin_record->pin;
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
manager_data->adc_handle = module->adc_handle;
manager_data->adc_channel = pin_record->channel;
manager_data->pwm_output = pin_record->pwm_output;
JS_ASSIGN_MULTI(mjs, manager) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, manager_data));
JS_FIELD("init", MJS_MK_FN(js_gpio_init));
JS_FIELD("write", MJS_MK_FN(js_gpio_write));
JS_FIELD("read", MJS_MK_FN(js_gpio_read));
JS_FIELD("readAnalog", MJS_MK_FN(js_gpio_read_analog));
JS_FIELD("interrupt", MJS_MK_FN(js_gpio_interrupt));
JS_FIELD("isPwmSupported", MJS_MK_FN(js_gpio_is_pwm_supported));
JS_FIELD("pwmWrite", MJS_MK_FN(js_gpio_pwm_write));
JS_FIELD("isPwmRunning", MJS_MK_FN(js_gpio_is_pwm_running));
JS_FIELD("pwmStop", MJS_MK_FN(js_gpio_pwm_stop));
}
mjs_return(mjs, manager);
// remember pin
ManagedPinsArray_push_back(module->managed_pins, manager_data);
}
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
if(M_UNLIKELY(!js_loop)) return NULL;
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
JsGpioInst* module = malloc(sizeof(JsGpioInst));
ManagedPinsArray_init(module->managed_pins);
module->adc_handle = furi_hal_adc_acquire();
module->loop = loop;
furi_hal_adc_configure(module->adc_handle);
mjs_val_t gpio_obj = mjs_mk_object(mjs);
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
*object = gpio_obj;
return (void*)module;
}
static void js_gpio_destroy(void* inst) {
furi_assert(inst);
JsGpioInst* module = (JsGpioInst*)inst;
// reset pins
for
M_EACH(item, module->managed_pins, ManagedPinsArray_t) {
JsGpioPinInst* manager_data = *item;
if(manager_data->had_interrupt) {
furi_hal_gpio_disable_int_callback(manager_data->pin);
furi_hal_gpio_remove_int_callback(manager_data->pin);
}
if(manager_data->pwm_output != FuriHalPwmOutputIdNone) {
if(furi_hal_pwm_is_running(manager_data->pwm_output))
furi_hal_pwm_stop(manager_data->pwm_output);
}
furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore);
furi_semaphore_free(manager_data->interrupt_semaphore);
free(manager_data->interrupt_contract);
free(manager_data);
}
// free buffers
furi_hal_adc_release(module->adc_handle);
ManagedPinsArray_clear(module->managed_pins);
free(module);
}
static const JsModuleDescriptor js_gpio_desc = {
"gpio",
js_gpio_create,
js_gpio_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gpio_desc,
};
const FlipperAppPluginDescriptor* js_gpio_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,169 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnMenuContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnMenuEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnMenuContext* context) {
UNUSED(context);
JsBtnMenuEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnMenuContext* context = ctx;
JsBtnMenuEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_header_assign(
struct mjs* mjs,
ButtonMenu* menu,
JsViewPropValue value,
JsBtnMenuContext* context) {
UNUSED(mjs);
button_menu_set_header(menu, str_buffer_make_owned_clone(&context->str_buffer, value.string));
return true;
}
static bool js_button_menu_add_child(
struct mjs* mjs,
ButtonMenu* menu,
JsBtnMenuContext* context,
mjs_val_t child_obj) {
static const JsValueEnumVariant js_button_menu_item_type_variants[] = {
{"common", ButtonMenuItemTypeCommon},
{"control", ButtonMenuItemTypeControl},
};
static const JsValueDeclaration js_button_menu_item_type =
JS_VALUE_ENUM(ButtonMenuItemType, js_button_menu_item_type_variants);
static const JsValueDeclaration js_button_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_menu_child_fields[] = {
{"type", &js_button_menu_item_type},
{"label", &js_button_menu_string},
};
static const JsValueDeclaration js_button_menu_child =
JS_VALUE_OBJECT(js_button_menu_child_fields);
ButtonMenuItemType item_type;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&item_type,
&label);
if(status != JsValueParseStatusOk) return false;
button_menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
context->next_index++,
input_callback,
item_type,
context);
return true;
}
static void js_button_menu_reset_children(ButtonMenu* menu, JsBtnMenuContext* context) {
context->next_index = 0;
button_menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnMenuContext* ctx_make(struct mjs* mjs, ButtonMenu* menu, mjs_val_t view_obj) {
UNUSED(menu);
JsBtnMenuContext* context = malloc(sizeof(JsBtnMenuContext));
*context = (JsBtnMenuContext){
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnMenuEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonMenu* input, JsBtnMenuContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_menu_alloc,
.free = (JsViewFree)button_menu_free,
.get_view = (JsViewGetView)button_menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_menu_add_child,
.reset_children = (JsViewResetChildren)js_button_menu_reset_children,
.prop_cnt = 1,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)matrix_header_assign},
}};
JS_GUI_VIEW_DEF(button_menu, &view_descriptor);

View File

@@ -1,274 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_panel.h>
#include <toolbox/str_buffer.h>
typedef struct {
size_t matrix_x, matrix_y;
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnPanelContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnPanelEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnPanelContext* context) {
UNUSED(context);
JsBtnPanelEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnPanelContext* context = ctx;
JsBtnPanelEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_size_x_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_x = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool matrix_size_y_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_y = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool js_button_panel_add_child(
struct mjs* mjs,
ButtonPanel* panel,
JsBtnPanelContext* context,
mjs_val_t child_obj) {
typedef enum {
JsButtonPanelChildTypeButton,
JsButtonPanelChildTypeLabel,
JsButtonPanelChildTypeIcon,
} JsButtonPanelChildType;
static const JsValueEnumVariant js_button_panel_child_type_variants[] = {
{"button", JsButtonPanelChildTypeButton},
{"label", JsButtonPanelChildTypeLabel},
{"icon", JsButtonPanelChildTypeIcon},
};
static const JsValueDeclaration js_button_panel_child_type =
JS_VALUE_ENUM(JsButtonPanelChildType, js_button_panel_child_type_variants);
static const JsValueDeclaration js_button_panel_number = JS_VALUE_SIMPLE(JsValueTypeInt32);
static const JsValueObjectField js_button_panel_common_fields[] = {
{"type", &js_button_panel_child_type},
{"x", &js_button_panel_number},
{"y", &js_button_panel_number},
};
static const JsValueDeclaration js_button_panel_common =
JS_VALUE_OBJECT(js_button_panel_common_fields);
static const JsValueDeclaration js_button_panel_pointer =
JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_button_panel_button_fields[] = {
{"matrixX", &js_button_panel_number},
{"matrixY", &js_button_panel_number},
{"icon", &js_button_panel_pointer},
{"iconSelected", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_button =
JS_VALUE_OBJECT(js_button_panel_button_fields);
static const JsValueDeclaration js_button_panel_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_panel_label_fields[] = {
{"text", &js_button_panel_string},
{"font", &js_gui_font_declaration},
};
static const JsValueDeclaration js_button_panel_label =
JS_VALUE_OBJECT(js_button_panel_label_fields);
static const JsValueObjectField js_button_panel_icon_fields[] = {
{"icon", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_icon =
JS_VALUE_OBJECT(js_button_panel_icon_fields);
JsButtonPanelChildType child_type;
int32_t x, y;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_common),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&child_type,
&x,
&y);
if(status != JsValueParseStatusOk) return false;
switch(child_type) {
case JsButtonPanelChildTypeButton: {
int32_t matrix_x, matrix_y;
const Icon *icon, *icon_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_button),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&matrix_x,
&matrix_y,
&icon,
&icon_selected);
if(status != JsValueParseStatusOk) return false;
button_panel_add_item(
panel,
context->next_index++,
matrix_x,
matrix_y,
x,
y,
icon,
icon_selected,
(ButtonItemCallback)input_callback,
context);
break;
}
case JsButtonPanelChildTypeLabel: {
const char* text;
Font font;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_label),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&text,
&font);
if(status != JsValueParseStatusOk) return false;
button_panel_add_label(
panel, x, y, font, str_buffer_make_owned_clone(&context->str_buffer, text));
break;
}
case JsButtonPanelChildTypeIcon: {
const Icon* icon;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_icon),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon);
if(status != JsValueParseStatusOk) return false;
button_panel_add_icon(panel, x, y, icon);
break;
}
}
return true;
}
static void js_button_panel_reset_children(ButtonPanel* panel, JsBtnPanelContext* context) {
context->next_index = 0;
button_panel_reset(panel);
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnPanelContext* ctx_make(struct mjs* mjs, ButtonPanel* panel, mjs_val_t view_obj) {
UNUSED(panel);
JsBtnPanelContext* context = malloc(sizeof(JsBtnPanelContext));
*context = (JsBtnPanelContext){
.matrix_x = 1,
.matrix_y = 1,
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnPanelEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonPanel* input, JsBtnPanelContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_panel_alloc,
.free = (JsViewFree)button_panel_free,
.get_view = (JsViewGetView)button_panel_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_panel_add_child,
.reset_children = (JsViewResetChildren)js_button_panel_reset_children,
.prop_cnt = 2,
.props = {
(JsViewPropDescriptor){
.name = "matrixSizeX",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_x_assign},
(JsViewPropDescriptor){
.name = "matrixSizeY",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_y_assign},
}};
JS_GUI_VIEW_DEF(button_panel, &view_descriptor);

View File

@@ -1,158 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/byte_input.h>
#define DEFAULT_BUF_SZ 4
typedef struct {
uint8_t* buffer;
size_t buffer_size;
size_t default_data_size;
FuriString* header;
FuriSemaphore* input_semaphore;
JsEventLoopContract contract;
} JsByteKbContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsByteKbContext* context) {
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
return mjs_mk_array_buf(mjs, (char*)context->buffer, context->buffer_size);
}
static void input_callback(JsByteKbContext* context) {
furi_semaphore_release(context->input_semaphore);
}
static bool header_assign(
struct mjs* mjs,
ByteInput* input,
JsViewPropValue value,
JsByteKbContext* context) {
UNUSED(mjs);
furi_string_set(context->header, value.string);
byte_input_set_header_text(input, furi_string_get_cstr(context->header));
return true;
}
static bool
len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) {
UNUSED(mjs);
UNUSED(input);
size_t new_buffer_size = value.number;
if(new_buffer_size < context->default_data_size) {
// Avoid confusing parameters from user
mjs_prepend_errorf(
mjs, MJS_BAD_ARGS_ERROR, "length must be larger than defaultData length");
return false;
}
context->buffer_size = new_buffer_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
byte_input_set_result_callback(
input,
(ByteInputCallback)input_callback,
NULL,
context,
context->buffer,
context->buffer_size);
return true;
}
static bool default_data_assign(
struct mjs* mjs,
ByteInput* input,
JsViewPropValue value,
JsByteKbContext* context) {
UNUSED(mjs);
mjs_val_t array_buf = value.term;
if(mjs_is_data_view(array_buf)) {
array_buf = mjs_dataview_get_buf(mjs, array_buf);
}
char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &context->default_data_size);
if(context->buffer_size < context->default_data_size) {
// Ensure buffer is large enough for defaultData
context->buffer_size = context->default_data_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
}
memcpy(context->buffer, (uint8_t*)default_data, context->default_data_size);
if(context->buffer_size > context->default_data_size) {
// Reset previous data after defaultData
memset(
context->buffer + context->default_data_size,
0x00,
context->buffer_size - context->default_data_size);
}
byte_input_set_result_callback(
input,
(ByteInputCallback)input_callback,
NULL,
context,
context->buffer,
context->buffer_size);
return true;
}
static JsByteKbContext* ctx_make(struct mjs* mjs, ByteInput* input, mjs_val_t view_obj) {
JsByteKbContext* context = malloc(sizeof(JsByteKbContext));
*context = (JsByteKbContext){
.buffer_size = DEFAULT_BUF_SZ,
.buffer = malloc(DEFAULT_BUF_SZ),
.header = furi_string_alloc(),
.input_semaphore = furi_semaphore_alloc(1, 0),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->input_semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
byte_input_set_result_callback(
input,
(ByteInputCallback)input_callback,
NULL,
context,
context->buffer,
context->buffer_size);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ByteInput* input, JsByteKbContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
furi_semaphore_free(context->input_semaphore);
furi_string_free(context->header);
free(context->buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)byte_input_alloc,
.free = (JsViewFree)byte_input_free,
.get_view = (JsViewGetView)byte_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "length",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)len_assign},
(JsViewPropDescriptor){
.name = "defaultData",
.type = JsViewPropTypeTypedArr,
.assign = (JsViewPropAssign)default_data_assign},
}};
JS_GUI_VIEW_DEF(byte_input, &view_descriptor);

View File

@@ -1,129 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/dialog_ex.h>
#define QUEUE_LEN 2
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsDialogCtx;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
UNUSED(context);
DialogExResult result;
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
const char* string;
if(result == DialogExResultLeft) {
string = "left";
} else if(result == DialogExResultCenter) {
string = "center";
} else if(result == DialogExResultRight) {
string = "right";
} else {
furi_crash();
}
return mjs_mk_string(mjs, string, ~0, false);
}
static void input_callback(DialogExResult result, JsDialogCtx* context) {
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
return true;
}
static bool
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
return true;
}
static bool
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_left_button_text(dialog, value.string);
return true;
}
static bool
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_center_button_text(dialog, value.string);
return true;
}
static bool
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_right_button_text(dialog, value.string);
return true;
}
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
dialog_ex_set_context(dialog, context);
return context;
}
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)dialog_ex_alloc,
.free = (JsViewFree)dialog_ex_free,
.get_view = (JsViewGetView)dialog_ex_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 5,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "left",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)left_assign},
(JsViewPropDescriptor){
.name = "center",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)center_assign},
(JsViewPropDescriptor){
.name = "right",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)right_assign},
}};
JS_GUI_VIEW_DEF(dialog, &view_descriptor);

View File

@@ -1,12 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include <gui/modules/empty_screen.h>
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)empty_screen_alloc,
.free = (JsViewFree)empty_screen_free,
.get_view = (JsViewGetView)empty_screen_get_view,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);

View File

@@ -1,53 +0,0 @@
#include "../../js_modules.h"
#include <dialogs/dialogs.h>
#include <assets_icons.h>
static void js_gui_file_picker_pick_file(struct mjs* mjs) {
static const JsValueDeclaration js_picker_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE(JsValueTypeString),
};
static const JsValueArguments js_picker_args = JS_VALUE_ARGS(js_picker_arg_list);
const char *base_path, *extension;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_picker_args, &base_path, &extension);
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
const DialogsFileBrowserOptions browser_options = {
.extension = extension,
.icon = &I_file_10px,
.base_path = base_path,
};
FuriString* path = furi_string_alloc_set(base_path);
if(dialog_file_browser_show(dialogs, path, path, &browser_options)) {
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true));
} else {
mjs_return(mjs, MJS_UNDEFINED);
}
furi_string_free(path);
furi_record_close(RECORD_DIALOGS);
}
static void* js_gui_file_picker_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
*object = mjs_mk_object(mjs);
mjs_set(mjs, *object, "pickFile", ~0, MJS_MK_FN(js_gui_file_picker_pick_file));
return NULL;
}
static const JsModuleDescriptor js_gui_file_picker_desc = {
"gui__file_picker",
js_gui_file_picker_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_file_picker_desc,
};
const FlipperAppPluginDescriptor* js_gui_file_picker_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,160 +0,0 @@
#include "../../js_modules.h"
#include <assets_icons.h>
#include <core/dangerous_defines.h>
#include <gui/icon_i.h>
#include <m-list.h>
typedef struct {
const char* name;
const Icon* data;
} IconDefinition;
#define ICON_DEF(icon) \
(IconDefinition) { \
.name = #icon, .data = &I_##icon \
}
#define ANIM_ICON_DEF(icon) \
(IconDefinition) { \
.name = #icon, .data = &A_##icon \
}
static const IconDefinition builtin_icons[] = {
// [NO_DOLPHIN] ICON_DEF(DolphinWait_59x54),
ICON_DEF(js_script_10px),
ICON_DEF(off_19x20),
ICON_DEF(off_hover_19x20),
ICON_DEF(power_19x20),
ICON_DEF(power_hover_19x20),
ANIM_ICON_DEF(Settings_14),
};
// Firmware's Icon struct needs a frames array, and uses a small CompressHeader
// Here we use a variable size allocation to add the uncompressed data in same allocation
// Also use a one-long array pointing to later in the same struct as the frames array
// CompressHeader includes a first is_compressed byte so we don't need to compress (.fxbm is uncompressed)
typedef struct FURI_PACKED {
Icon icon;
uint8_t* frames[1];
struct {
uint8_t is_compressed;
uint8_t uncompressed_data[];
} frame;
} FxbmIconWrapper;
LIST_DEF(FxbmIconWrapperList, FxbmIconWrapper*, M_PTR_OPLIST); // NOLINT
#define M_OPL_FxbmIconWrapperList_t() LIST_OPLIST(FxbmIconWrapperList)
typedef struct {
FxbmIconWrapperList_t fxbm_list;
} JsGuiIconInst;
static const JsValueDeclaration js_icon_get_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
};
static const JsValueArguments js_icon_get_args = JS_VALUE_ARGS(js_icon_get_arg_list);
static void js_gui_icon_get_builtin(struct mjs* mjs) {
const char* icon_name;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_icon_get_args, &icon_name);
for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) {
if(strcmp(icon_name, builtin_icons[i].name) == 0) {
mjs_return(mjs, mjs_mk_foreign(mjs, (void*)builtin_icons[i].data));
return;
}
}
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "no such built-in icon");
}
static void js_gui_icon_load_fxbm(struct mjs* mjs) {
const char* fxbm_path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_icon_get_args, &fxbm_path);
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
FxbmIconWrapper* fxbm = NULL;
do {
if(!storage_file_open(file, fxbm_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
break;
}
struct {
uint32_t size; // Total following size including width and height values
uint32_t width;
uint32_t height;
} fxbm_header;
if(storage_file_read(file, &fxbm_header, sizeof(fxbm_header)) != sizeof(fxbm_header)) {
break;
}
size_t frame_size = fxbm_header.size - sizeof(uint32_t) * 2;
fxbm = malloc(sizeof(FxbmIconWrapper) + frame_size);
if(storage_file_read(file, fxbm->frame.uncompressed_data, frame_size) != frame_size) {
free(fxbm);
fxbm = NULL;
break;
}
FURI_CONST_ASSIGN(fxbm->icon.width, fxbm_header.width);
FURI_CONST_ASSIGN(fxbm->icon.height, fxbm_header.height);
FURI_CONST_ASSIGN(fxbm->icon.frame_count, 1);
FURI_CONST_ASSIGN(fxbm->icon.frame_rate, 1);
FURI_CONST_ASSIGN_PTR(fxbm->icon.frames, fxbm->frames);
fxbm->frames[0] = (void*)&fxbm->frame;
fxbm->frame.is_compressed = false;
} while(false);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(!fxbm) {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "could not load .fxbm icon");
}
JsGuiIconInst* js_icon = JS_GET_CONTEXT(mjs);
FxbmIconWrapperList_push_back(js_icon->fxbm_list, fxbm);
mjs_return(mjs, mjs_mk_foreign(mjs, (void*)&fxbm->icon));
}
static void* js_gui_icon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsGuiIconInst* js_icon = malloc(sizeof(JsGuiIconInst));
FxbmIconWrapperList_init(js_icon->fxbm_list);
*object = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, *object) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, js_icon));
JS_FIELD("getBuiltin", MJS_MK_FN(js_gui_icon_get_builtin));
JS_FIELD("loadFxbm", MJS_MK_FN(js_gui_icon_load_fxbm));
}
return js_icon;
}
static void js_gui_icon_destroy(void* inst) {
JsGuiIconInst* js_icon = inst;
for
M_EACH(fxbm, js_icon->fxbm_list, FxbmIconWrapperList_t) {
free(*fxbm);
}
FxbmIconWrapperList_clear(js_icon->fxbm_list);
free(js_icon);
}
static const JsModuleDescriptor js_gui_icon_desc = {
"gui__icon",
js_gui_icon_create,
js_gui_icon_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_icon_desc,
};
const FlipperAppPluginDescriptor* js_gui_icon_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,495 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "./js_gui.h"
#include <furi.h>
#include <mlib/m-array.h>
#include <gui/view_dispatcher.h>
#include "../js_event_loop/js_event_loop.h"
#include <m-array.h>
#define EVENT_QUEUE_SIZE 16
typedef struct {
uint32_t next_view_id;
FuriEventLoop* loop;
Gui* gui;
ViewDispatcher* dispatcher;
// event stuff
JsEventLoopContract custom_contract;
FuriMessageQueue* custom;
JsEventLoopContract navigation_contract;
FuriSemaphore*
navigation; // FIXME: (-nofl) convert into callback once FuriEventLoop starts supporting this
} JsGui;
// Useful for factories
static JsGui* js_gui;
typedef struct {
uint32_t id;
const JsViewDescriptor* descriptor;
void* specific_view;
void* custom_data;
} JsGuiViewData;
static const JsValueEnumVariant js_gui_font_variants[] = {
{"primary", FontPrimary},
{"secondary", FontSecondary},
{"keyboard", FontKeyboard},
{"bit_numbers", FontBigNumbers},
};
const JsValueDeclaration js_gui_font_declaration = JS_VALUE_ENUM(Font, js_gui_font_variants);
/**
* @brief Transformer for custom events
*/
static mjs_val_t
js_gui_vd_custom_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
UNUSED(context);
furi_check(object);
FuriMessageQueue* queue = object;
uint32_t event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
return mjs_mk_number(mjs, (double)event);
}
/**
* @brief ViewDispatcher custom event callback
*/
static bool js_gui_vd_custom_callback(void* context, uint32_t event) {
furi_check(context);
JsGui* module = context;
furi_check(furi_message_queue_put(module->custom, &event, 0) == FuriStatusOk);
return true;
}
/**
* @brief ViewDispatcher navigation event callback
*/
static bool js_gui_vd_nav_callback(void* context) {
furi_check(context);
JsGui* module = context;
furi_semaphore_release(module->navigation);
return true;
}
/**
* @brief `viewDispatcher.sendCustom`
*/
static void js_gui_vd_send_custom(struct mjs* mjs) {
static const JsValueDeclaration js_gui_vd_send_custom_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_gui_vd_send_custom_args =
JS_VALUE_ARGS(js_gui_vd_send_custom_arg_list);
int32_t event;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_send_custom_args, &event);
JsGui* module = JS_GET_CONTEXT(mjs);
view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event);
}
/**
* @brief `viewDispatcher.sendTo`
*/
static void js_gui_vd_send_to(struct mjs* mjs) {
typedef enum {
JsSendDirToFront,
JsSendDirToBack,
} JsSendDir;
static const JsValueEnumVariant js_send_dir_variants[] = {
{"front", JsSendDirToFront},
{"back", JsSendDirToBack},
};
static const JsValueDeclaration js_gui_vd_send_to_arg_list[] = {
JS_VALUE_ENUM(JsSendDir, js_send_dir_variants),
};
static const JsValueArguments js_gui_vd_send_to_args =
JS_VALUE_ARGS(js_gui_vd_send_to_arg_list);
JsSendDir send_direction;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_send_to_args, &send_direction);
JsGui* module = JS_GET_CONTEXT(mjs);
if(send_direction == JsSendDirToBack) {
view_dispatcher_send_to_back(module->dispatcher);
} else {
view_dispatcher_send_to_front(module->dispatcher);
}
}
/**
* @brief `viewDispatcher.switchTo`
*/
static void js_gui_vd_switch_to(struct mjs* mjs) {
static const JsValueDeclaration js_gui_vd_switch_to_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_gui_vd_switch_to_args =
JS_VALUE_ARGS(js_gui_vd_switch_to_arg_list);
mjs_val_t view;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_switch_to_args, &view);
JsGuiViewData* view_data = JS_GET_INST(mjs, view);
mjs_val_t vd_obj = mjs_get_this(mjs);
JsGui* module = JS_GET_INST(mjs, vd_obj);
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
mjs_set(mjs, vd_obj, "currentView", ~0, view);
}
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
// get event loop
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
if(M_UNLIKELY(!js_loop)) return NULL;
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
// create C object
JsGui* module = malloc(sizeof(JsGui));
module->loop = loop;
module->gui = furi_record_open(RECORD_GUI);
module->dispatcher = view_dispatcher_alloc_ex(loop);
module->custom = furi_message_queue_alloc(EVENT_QUEUE_SIZE, sizeof(uint32_t));
module->navigation = furi_semaphore_alloc(EVENT_QUEUE_SIZE, 0);
view_dispatcher_attach_to_gui(module->dispatcher, module->gui, ViewDispatcherTypeFullscreen);
view_dispatcher_send_to_front(module->dispatcher);
// subscribe to events and create contracts
view_dispatcher_set_event_callback_context(module->dispatcher, module);
view_dispatcher_set_custom_event_callback(module->dispatcher, js_gui_vd_custom_callback);
view_dispatcher_set_navigation_event_callback(module->dispatcher, js_gui_vd_nav_callback);
module->custom_contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object = module->custom,
.object_type = JsEventLoopObjectTypeQueue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = js_gui_vd_custom_transformer,
},
};
module->navigation_contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object = module->navigation,
.object_type = JsEventLoopObjectTypeSemaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
// create viewDispatcher object
mjs_val_t view_dispatcher = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, view_dispatcher) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
JS_FIELD("sendCustom", MJS_MK_FN(js_gui_vd_send_custom));
JS_FIELD("sendTo", MJS_MK_FN(js_gui_vd_send_to));
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
JS_FIELD("currentView", MJS_NULL);
}
// create API object
mjs_val_t api = mjs_mk_object(mjs);
mjs_set(mjs, api, "viewDispatcher", ~0, view_dispatcher);
*object = api;
js_gui = module;
return module;
}
static void js_gui_destroy(void* inst) {
furi_assert(inst);
JsGui* module = inst;
view_dispatcher_free(module->dispatcher);
furi_event_loop_maybe_unsubscribe(module->loop, module->custom);
furi_event_loop_maybe_unsubscribe(module->loop, module->navigation);
furi_message_queue_free(module->custom);
furi_semaphore_free(module->navigation);
furi_record_close(RECORD_GUI);
free(module);
js_gui = NULL;
}
/**
* @brief Assigns a `View` property. Not available from JS.
*/
static bool
js_gui_view_assign(struct mjs* mjs, const char* name, mjs_val_t value, JsGuiViewData* data) {
const JsViewDescriptor* descriptor = data->descriptor;
for(size_t i = 0; i < descriptor->prop_cnt; i++) {
JsViewPropDescriptor prop = descriptor->props[i];
if(strcmp(prop.name, name) != 0) continue;
// convert JS value to C
JsViewPropValue c_value;
const char* expected_type = NULL;
switch(prop.type) {
case JsViewPropTypeNumber: {
if(!mjs_is_number(value)) {
expected_type = "number";
break;
}
c_value = (JsViewPropValue){.number = mjs_get_int32(mjs, value)};
} break;
case JsViewPropTypeString: {
if(!mjs_is_string(value)) {
expected_type = "string";
break;
}
c_value = (JsViewPropValue){.string = mjs_get_string(mjs, &value, NULL)};
} break;
case JsViewPropTypeArr: {
if(!mjs_is_array(value)) {
expected_type = "array";
break;
}
c_value = (JsViewPropValue){.term = value};
} break;
case JsViewPropTypeTypedArr: {
if(!mjs_is_typed_array(value)) {
expected_type = "typed_array";
break;
}
c_value = (JsViewPropValue){.term = value};
} break;
case JsViewPropTypeBool: {
if(!mjs_is_boolean(value)) {
expected_type = "bool";
break;
}
c_value = (JsViewPropValue){.boolean = mjs_get_bool(mjs, value)};
} break;
}
if(expected_type) {
mjs_prepend_errorf(
mjs, MJS_BAD_ARGS_ERROR, "view prop \"%s\" requires %s value", name, expected_type);
return false;
} else {
return prop.assign(mjs, data->specific_view, c_value, data->custom_data);
}
}
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "view has no prop named \"%s\"", name);
return false;
}
/**
* @brief Sets the list of children. Not available from JS.
*/
static bool js_gui_view_internal_set_children(
struct mjs* mjs,
mjs_val_t children,
JsGuiViewData* data,
bool do_reset) {
if(do_reset) data->descriptor->reset_children(data->specific_view, data->custom_data);
for(size_t i = 0; i < mjs_array_length(mjs, children); i++) {
mjs_val_t child = mjs_array_get(mjs, children, i);
if(!data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child))
return false;
}
return true;
}
/**
* @brief `View.set`
*/
static void js_gui_view_set(struct mjs* mjs) {
static const JsValueDeclaration js_gui_view_set_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_gui_view_set_args = JS_VALUE_ARGS(js_gui_view_set_arg_list);
const char* name;
mjs_val_t value;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_set_args, &name, &value);
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
bool success = js_gui_view_assign(mjs, name, value, data);
UNUSED(success);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.addChild`
*/
static void js_gui_view_add_child(struct mjs* mjs) {
static const JsValueDeclaration js_gui_view_add_child_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_gui_view_add_child_args =
JS_VALUE_ARGS(js_gui_view_add_child_arg_list);
mjs_val_t child;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_add_child_args, &child);
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child);
UNUSED(success);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.resetChildren`
*/
static void js_gui_view_reset_children(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
data->descriptor->reset_children(data->specific_view, data->custom_data);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.setChildren`
*/
static void js_gui_view_set_children(struct mjs* mjs) {
static const JsValueDeclaration js_gui_view_set_children_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAnyArray),
};
static const JsValueArguments js_gui_view_set_children_args =
JS_VALUE_ARGS(js_gui_view_set_children_arg_list);
mjs_val_t children;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_set_children_args, &children);
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
js_gui_view_internal_set_children(mjs, children, data, true);
}
/**
* @brief `View` destructor
*/
static void js_gui_view_destructor(struct mjs* mjs, mjs_val_t obj) {
JsGuiViewData* data = JS_GET_INST(mjs, obj);
view_dispatcher_remove_view(js_gui->dispatcher, data->id);
if(data->descriptor->custom_destroy)
data->descriptor->custom_destroy(data->specific_view, data->custom_data, js_gui->loop);
data->descriptor->free(data->specific_view);
free(data);
}
/**
* @brief Creates a `View` object from a descriptor. Not available from JS.
*/
static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descriptor) {
void* specific_view = descriptor->alloc();
View* view = descriptor->get_view(specific_view);
uint32_t view_id = js_gui->next_view_id++;
view_dispatcher_add_view(js_gui->dispatcher, view_id, view);
// generic view API
mjs_val_t view_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, view_obj) {
JS_FIELD("set", MJS_MK_FN(js_gui_view_set));
JS_FIELD("addChild", MJS_MK_FN(js_gui_view_add_child));
JS_FIELD("resetChildren", MJS_MK_FN(js_gui_view_reset_children));
JS_FIELD("setChildren", MJS_MK_FN(js_gui_view_set_children));
}
// object data
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
*data = (JsGuiViewData){
.descriptor = descriptor,
.id = view_id,
.specific_view = specific_view,
.custom_data =
descriptor->custom_make ? descriptor->custom_make(mjs, specific_view, view_obj) : NULL,
};
mjs_set(mjs, view_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, data));
mjs_set(mjs, view_obj, MJS_DESTRUCTOR_PROP_NAME, ~0, MJS_MK_FN(js_gui_view_destructor));
return view_obj;
}
/**
* @brief `ViewFactory.make`
*/
static void js_gui_vf_make(struct mjs* mjs) {
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
mjs_return(mjs, js_gui_make_view(mjs, descriptor));
}
/**
* @brief `ViewFactory.makeWith`
*/
static void js_gui_vf_make_with(struct mjs* mjs) {
static const JsValueDeclaration js_gui_vf_make_with_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAnyObject),
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_gui_vf_make_with_args =
JS_VALUE_ARGS(js_gui_vf_make_with_arg_list);
mjs_val_t props, children;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vf_make_with_args, &props, &children);
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
// make the object like normal
mjs_val_t view_obj = js_gui_make_view(mjs, descriptor);
JsGuiViewData* data = JS_GET_INST(mjs, view_obj);
// assign properties one by one
mjs_val_t key, iter = MJS_UNDEFINED;
while((key = mjs_next(mjs, props, &iter)) != MJS_UNDEFINED) {
furi_check(mjs_is_string(key));
const char* name = mjs_get_string(mjs, &key, NULL);
mjs_val_t value = mjs_get(mjs, props, name, ~0);
if(!js_gui_view_assign(mjs, name, value, data)) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
// assign children
if(mjs_is_array(children)) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
if(!js_gui_view_internal_set_children(mjs, children, data, false)) return;
}
mjs_return(mjs, view_obj);
}
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor) {
mjs_val_t factory = mjs_mk_object(mjs);
mjs_set(mjs, factory, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, (void*)view_descriptor));
mjs_set(mjs, factory, "make", ~0, MJS_MK_FN(js_gui_vf_make));
mjs_set(mjs, factory, "makeWith", ~0, MJS_MK_FN(js_gui_vf_make_with));
return factory;
}
extern const ElfApiInterface js_gui_hashtable_api_interface;
static const JsModuleDescriptor js_gui_desc = {
"gui",
js_gui_create,
js_gui_destroy,
&js_gui_hashtable_api_interface,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_desc,
};
const FlipperAppPluginDescriptor* js_gui_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,136 +0,0 @@
#include "../../js_modules.h"
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
JsViewPropTypeString,
JsViewPropTypeNumber,
JsViewPropTypeArr,
JsViewPropTypeTypedArr,
JsViewPropTypeBool,
} JsViewPropType;
typedef union {
const char* string;
int32_t number;
bool boolean;
mjs_val_t term;
} JsViewPropValue;
/**
* JS-to-C font enum mapping
*/
extern const JsValueDeclaration js_gui_font_declaration;
/**
* @brief Assigns a value to a view property
*
* The name and the type are implicit and defined in the property descriptor
*/
typedef bool (
*JsViewPropAssign)(struct mjs* mjs, void* specific_view, JsViewPropValue value, void* context);
/** @brief Property descriptor */
typedef struct {
const char* name; //<! Property name, as visible from JS
JsViewPropType type; // <! Property type, ensured by the GUI module
JsViewPropAssign assign; // <! Property assignment callback
} JsViewPropDescriptor;
// View method signatures
/** @brief View's `_alloc` method */
typedef void* (*JsViewAlloc)(void);
/** @brief View's `_get_view` method */
typedef View* (*JsViewGetView)(void* specific_view);
/** @brief View's `_free` method */
typedef void (*JsViewFree)(void* specific_view);
// Glue code method signatures
/** @brief Context instantiation for glue code */
typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj);
/** @brief Context destruction for glue code */
typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop);
/** @brief `addChild` callback for glue code */
typedef bool (
*JsViewAddChild)(struct mjs* mjs, void* specific_view, void* custom_state, mjs_val_t child_obj);
/** @brief `resetChildren` callback for glue code */
typedef void (*JsViewResetChildren)(void* specific_view, void* custom_state);
/**
* @brief Descriptor for a JS view
*
* Contains:
* - Pointers to generic view methods (`alloc`, `get_view` and `free`)
* - Pointers to glue code context ctor/dtor methods (`custom_make`,
* `custom_destroy`)
* - Descriptors of properties visible from JS (`prop_cnt`, `props`)
*
* `js_gui` uses this descriptor to produce view factories and views.
*/
typedef struct {
JsViewAlloc alloc;
JsViewGetView get_view;
JsViewFree free;
JsViewCustomMake custom_make; // <! May be NULL
JsViewCustomDestroy custom_destroy; // <! May be NULL
JsViewAddChild add_child; // <! May be NULL
JsViewResetChildren reset_children; // <! May be NULL
size_t prop_cnt; //<! Number of properties visible from JS
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
} JsViewDescriptor;
// Callback ordering:
// +-> add_child -+
// +-> reset_children -+
// alloc -> get_view -> custom_make -+-> props[i].assign -+> custom_destroy -> free
// \__________ creation __________/ \____ use ____/ \___ destruction ____/
/**
* @brief Creates a JS `ViewFactory` object
*
* This function is intended to be used by individual view adapter modules that
* wish to create a unified JS API interface in a declarative way. Usually this
* is done via the `JS_GUI_VIEW_DEF` macro which hides all the boilerplate.
*
* The `ViewFactory` object exposes two methods, `make` and `makeWith`, each
* returning a `View` object. These objects fully comply with the expectations
* of the `ViewDispatcher`, TS type definitions and the proposed Flipper JS
* coding style.
*/
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor);
/**
* @brief Defines a module implementing `View` glue code
*/
#define JS_GUI_VIEW_DEF(name, descriptor) \
static void* view_mod_ctor(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { \
UNUSED(modules); \
*object = js_gui_make_view_factory(mjs, descriptor); \
return NULL; \
} \
static const JsModuleDescriptor js_mod_desc = { \
"gui__" #name, \
view_mod_ctor, \
NULL, \
NULL, \
}; \
static const FlipperAppPluginDescriptor plugin_descriptor = { \
.appid = PLUGIN_APP_ID, \
.ep_api_version = PLUGIN_API_VERSION, \
.entry_point = &js_mod_desc, \
}; \
const FlipperAppPluginDescriptor* js_view_##name##_ep(void) { \
return &plugin_descriptor; \
}
#ifdef __cplusplus
}
#endif

View File

@@ -1,16 +0,0 @@
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/api_hashtable/compilesort.hpp>
#include "js_gui_api_table_i.h"
static_assert(!has_hash_collisions(js_gui_api_table), "Detected API method hash collision!");
extern "C" constexpr HashtableApiInterface js_gui_hashtable_api_interface{
{
.api_version_major = 0,
.api_version_minor = 0,
.resolver_callback = &elf_resolve_from_hashtable,
},
js_gui_api_table.cbegin(),
js_gui_api_table.cend(),
};

View File

@@ -1,5 +0,0 @@
#include "js_gui.h"
static constexpr auto js_gui_api_table = sort(create_array_t<sym_entry>(
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)),
API_VARIABLE(js_gui_font_declaration, const JsValueDeclaration)));

View File

@@ -1,12 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include <gui/modules/loading.h>
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)loading_alloc,
.free = (JsViewFree)loading_free,
.get_view = (JsViewGetView)loading_get_view,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(loading, &view_descriptor);

View File

@@ -1,105 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsMenuCtx;
static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) {
UNUSED(context);
uint32_t index;
furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk);
return mjs_mk_number(mjs, (double)index);
}
static void choose_callback(void* context, uint32_t index) {
JsMenuCtx* ctx = context;
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
}
static bool
js_menu_add_child(struct mjs* mjs, Menu* menu, JsMenuCtx* context, mjs_val_t child_obj) {
static const JsValueDeclaration js_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueDeclaration js_menu_pointer = JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_menu_child_fields[] = {
{"icon", &js_menu_pointer},
{"label", &js_menu_string},
};
static const JsValueDeclaration js_menu_child = JS_VALUE_OBJECT(js_menu_child_fields);
const Icon* icon;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon,
&label);
if(status != JsValueParseStatusOk) return false;
menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
icon,
context->next_index++,
choose_callback,
context);
return true;
}
static void js_menu_reset_children(Menu* menu, JsMenuCtx* context) {
context->next_index = 0;
menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsMenuCtx* ctx_make(struct mjs* mjs, Menu* input, mjs_val_t view_obj) {
UNUSED(input);
JsMenuCtx* context = malloc(sizeof(JsMenuCtx));
context->queue = furi_message_queue_alloc(1, sizeof(uint32_t));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)choose_transformer,
},
};
mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(Menu* input, JsMenuCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)menu_alloc,
.free = (JsViewFree)menu_free,
.get_view = (JsViewGetView)menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_menu_add_child,
.reset_children = (JsViewResetChildren)js_menu_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(menu, &view_descriptor);

View File

@@ -1,130 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/number_input.h>
typedef struct {
int32_t default_val, min_val, max_val;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsNumKbdContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsNumKbdContext* context) {
UNUSED(context);
int32_t number;
furi_check(furi_message_queue_get(queue, &number, 0) == FuriStatusOk);
return mjs_mk_number(mjs, number);
}
static void input_callback(void* ctx, int32_t value) {
JsNumKbdContext* context = ctx;
furi_check(furi_message_queue_put(context->input_queue, &value, 0) == FuriStatusOk);
}
static bool header_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
UNUSED(context);
number_input_set_header_text(input, value.string);
return true;
}
static bool min_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->min_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static bool max_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->max_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static bool default_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->default_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static JsNumKbdContext* ctx_make(struct mjs* mjs, NumberInput* input, mjs_val_t view_obj) {
JsNumKbdContext* context = malloc(sizeof(JsNumKbdContext));
*context = (JsNumKbdContext){
.default_val = 0,
.max_val = 100,
.min_val = 0,
.input_queue = furi_message_queue_alloc(1, sizeof(int32_t)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(NumberInput* input, JsNumKbdContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)number_input_alloc,
.free = (JsViewFree)number_input_free,
.get_view = (JsViewGetView)number_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 4,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "minValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)min_val_assign},
(JsViewPropDescriptor){
.name = "maxValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_val_assign},
(JsViewPropDescriptor){
.name = "defaultValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)default_val_assign},
}};
JS_GUI_VIEW_DEF(number_input, &view_descriptor);

View File

@@ -1,102 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/popup.h>
#include <toolbox/str_buffer.h>
typedef struct {
StrBuffer str_buffer;
FuriSemaphore* semaphore;
JsEventLoopContract contract;
} JsPopupCtx;
static void timeout_callback(JsPopupCtx* context) {
furi_check(furi_semaphore_release(context->semaphore) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_header(
popup,
str_buffer_make_owned_clone(&context->str_buffer, value.string),
64,
0,
AlignCenter,
AlignTop);
return true;
}
static bool
text_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_text(
popup,
str_buffer_make_owned_clone(&context->str_buffer, value.string),
64,
32,
AlignCenter,
AlignCenter);
return true;
}
static bool
timeout_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_timeout(popup, value.number);
popup_enable_timeout(popup);
return true;
}
static JsPopupCtx* ctx_make(struct mjs* mjs, Popup* popup, mjs_val_t view_obj) {
JsPopupCtx* context = malloc(sizeof(JsPopupCtx));
context->semaphore = furi_semaphore_alloc(1, 0);
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
mjs_set(mjs, view_obj, "timeout", ~0, mjs_mk_foreign(mjs, &context->contract));
popup_set_callback(popup, (PopupCallback)timeout_callback);
popup_set_context(popup, context);
return context;
}
static void ctx_destroy(Popup* popup, JsPopupCtx* context, FuriEventLoop* loop) {
UNUSED(popup);
furi_event_loop_maybe_unsubscribe(loop, context->semaphore);
furi_semaphore_free(context->semaphore);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)popup_alloc,
.free = (JsViewFree)popup_free,
.get_view = (JsViewGetView)popup_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "timeout",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)timeout_assign},
}};
JS_GUI_VIEW_DEF(popup, &view_descriptor);

View File

@@ -1,92 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/submenu.h>
#define QUEUE_LEN 2
typedef struct {
int32_t next_index;
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsSubmenuCtx;
static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) {
UNUSED(context);
uint32_t index;
furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk);
return mjs_mk_number(mjs, (double)index);
}
void choose_callback(void* context, uint32_t index) {
JsSubmenuCtx* ctx = context;
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
UNUSED(mjs);
UNUSED(context);
submenu_set_header(submenu, value.string);
return true;
}
static bool js_submenu_add_child(
struct mjs* mjs,
Submenu* submenu,
JsSubmenuCtx* context,
mjs_val_t child_obj) {
const char* str = mjs_get_string(mjs, &child_obj, NULL);
if(!str) return false;
submenu_add_item(submenu, str, context->next_index++, choose_callback, context);
return true;
}
static void js_submenu_reset_children(Submenu* submenu, JsSubmenuCtx* context) {
context->next_index = 0;
submenu_reset(submenu);
}
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
UNUSED(input);
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(uint32_t));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)choose_transformer,
},
};
mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(Submenu* input, JsSubmenuCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)submenu_alloc,
.free = (JsViewFree)submenu_free,
.get_view = (JsViewGetView)submenu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_submenu_add_child,
.reset_children = (JsViewResetChildren)js_submenu_reset_children,
.prop_cnt = 1,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
}};
JS_GUI_VIEW_DEF(submenu, &view_descriptor);

View File

@@ -1,78 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include <gui/modules/text_box.h>
static bool
text_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, FuriString* context) {
UNUSED(mjs);
furi_string_set(context, value.string);
text_box_set_text(text_box, furi_string_get_cstr(context));
return true;
}
static bool font_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
UNUSED(context);
TextBoxFont font;
if(strcasecmp(value.string, "hex") == 0) {
font = TextBoxFontHex;
} else if(strcasecmp(value.string, "text") == 0) {
font = TextBoxFontText;
} else {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"text\", \"hex\"");
return false;
}
text_box_set_font(text_box, font);
return true;
}
static bool
focus_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
UNUSED(context);
TextBoxFocus focus;
if(strcasecmp(value.string, "start") == 0) {
focus = TextBoxFocusStart;
} else if(strcasecmp(value.string, "end") == 0) {
focus = TextBoxFocusEnd;
} else {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"start\", \"end\"");
return false;
}
text_box_set_focus(text_box, focus);
return true;
}
FuriString* ctx_make(struct mjs* mjs, TextBox* specific_view, mjs_val_t view_obj) {
UNUSED(mjs);
UNUSED(specific_view);
UNUSED(view_obj);
return furi_string_alloc();
}
void ctx_destroy(TextBox* specific_view, FuriString* context, FuriEventLoop* loop) {
UNUSED(specific_view);
UNUSED(loop);
furi_string_free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)text_box_alloc,
.free = (JsViewFree)text_box_free,
.get_view = (JsViewGetView)text_box_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.props = {
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "font",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)font_assign},
(JsViewPropDescriptor){
.name = "focus",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)focus_assign},
}};
JS_GUI_VIEW_DEF(text_box, &view_descriptor);

View File

@@ -1,188 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/text_input.h>
#define DEFAULT_BUF_SZ 33
typedef struct {
char* buffer;
size_t buffer_size;
size_t default_text_size;
FuriString* header;
bool default_text_clear;
FuriSemaphore* input_semaphore;
JsEventLoopContract contract;
} JsKbdContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsKbdContext* context) {
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
return mjs_mk_string(mjs, context->buffer, ~0, true);
}
static void input_callback(JsKbdContext* context) {
furi_semaphore_release(context->input_semaphore);
}
static bool
header_assign(struct mjs* mjs, TextInput* input, JsViewPropValue value, JsKbdContext* context) {
UNUSED(mjs);
furi_string_set(context->header, value.string);
text_input_set_header_text(input, furi_string_get_cstr(context->header));
return true;
}
static bool min_len_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
UNUSED(context);
text_input_set_minimum_length(input, (size_t)value.number);
return true;
}
static bool max_len_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
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,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
return true;
}
static bool default_text_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
UNUSED(input);
if(value.string) {
context->default_text_size = strlen(value.string) + 1;
if(context->buffer_size < context->default_text_size) {
// Ensure buffer is large enough for defaultData
context->buffer_size = context->default_text_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
}
// Also trim excess previous data with strlcpy()
strlcpy(context->buffer, value.string, context->buffer_size); //-V575
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
}
return true;
}
static bool default_text_clear_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
context->default_text_clear = value.boolean;
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
return true;
}
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
JsKbdContext* context = malloc(sizeof(JsKbdContext));
*context = (JsKbdContext){
.buffer_size = DEFAULT_BUF_SZ,
.buffer = malloc(DEFAULT_BUF_SZ),
.header = furi_string_alloc(),
.default_text_clear = false,
.input_semaphore = furi_semaphore_alloc(1, 0),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->input_semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(TextInput* input, JsKbdContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
furi_semaphore_free(context->input_semaphore);
furi_string_free(context->header);
free(context->buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)text_input_alloc,
.free = (JsViewFree)text_input_free,
.get_view = (JsViewGetView)text_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 5,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "minLength",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)min_len_assign},
(JsViewPropDescriptor){
.name = "maxLength",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_len_assign},
(JsViewPropDescriptor){
.name = "defaultText",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)default_text_assign},
(JsViewPropDescriptor){
.name = "defaultTextClear",
.type = JsViewPropTypeBool,
.assign = (JsViewPropAssign)default_text_clear_assign},
}};
JS_GUI_VIEW_DEF(text_input, &view_descriptor);

View File

@@ -1,163 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/variable_item_list.h>
#include <toolbox/str_buffer.h>
typedef struct {
StrBuffer str_buffer;
// let mjs do the memory management heavy lifting, store children in a js array
struct mjs* mjs;
mjs_val_t children;
VariableItemList* list;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsViListContext;
typedef struct {
int32_t item_index;
int32_t value_index;
} JsViListEvent;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsViListContext* context) {
UNUSED(context);
JsViListEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("itemIndex", mjs_mk_number(mjs, event.item_index));
JS_FIELD("valueIndex", mjs_mk_number(mjs, event.value_index));
}
return event_obj;
}
static void js_vi_list_change_callback(VariableItem* item) {
JsViListContext* context = variable_item_get_context(item);
struct mjs* mjs = context->mjs;
uint8_t item_index = variable_item_list_get_selected_item_index(context->list);
uint8_t value_index = variable_item_get_current_value_index(item);
// type safety ensured in add_child
mjs_val_t variants = mjs_array_get(mjs, context->children, item_index);
mjs_val_t variant = mjs_array_get(mjs, variants, value_index);
variable_item_set_current_value_text(item, mjs_get_string(mjs, &variant, NULL));
JsViListEvent event = {
.item_index = item_index,
.value_index = value_index,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool js_vi_list_add_child(
struct mjs* mjs,
VariableItemList* list,
JsViListContext* context,
mjs_val_t child_obj) {
static const JsValueDeclaration js_vi_list_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueDeclaration js_vi_list_arr = JS_VALUE_SIMPLE(JsValueTypeAnyArray);
static const JsValueDeclaration js_vi_list_int_default_0 =
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 0);
static const JsValueObjectField js_vi_list_child_fields[] = {
{"label", &js_vi_list_string},
{"variants", &js_vi_list_arr},
{"defaultSelected", &js_vi_list_int_default_0},
};
static const JsValueDeclaration js_vi_list_child =
JS_VALUE_OBJECT_W_DEFAULTS(js_vi_list_child_fields);
JsValueParseStatus status;
const char* label;
mjs_val_t variants;
int32_t default_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_vi_list_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&label,
&variants,
&default_selected);
if(status != JsValueParseStatusOk) return false;
size_t variants_cnt = mjs_array_length(mjs, variants);
for(size_t i = 0; i < variants_cnt; i++)
if(!mjs_is_string(mjs_array_get(mjs, variants, i))) return false;
VariableItem* item = variable_item_list_add(
list,
str_buffer_make_owned_clone(&context->str_buffer, label),
variants_cnt,
js_vi_list_change_callback,
context);
variable_item_set_current_value_index(item, default_selected);
mjs_val_t default_variant = mjs_array_get(mjs, variants, default_selected);
variable_item_set_current_value_text(item, mjs_get_string(mjs, &default_variant, NULL));
mjs_array_push(context->mjs, context->children, variants);
return true;
}
static void js_vi_list_reset_children(VariableItemList* list, JsViListContext* context) {
mjs_disown(context->mjs, &context->children);
context->children = mjs_mk_array(context->mjs);
mjs_own(context->mjs, &context->children);
variable_item_list_reset(list);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsViListContext* ctx_make(struct mjs* mjs, VariableItemList* list, mjs_val_t view_obj) {
JsViListContext* context = malloc(sizeof(JsViListContext));
*context = (JsViListContext){
.str_buffer = {0},
.mjs = mjs,
.children = mjs_mk_array(mjs),
.list = list,
.input_queue = furi_message_queue_alloc(1, sizeof(JsViListEvent)),
};
mjs_own(context->mjs, &context->children);
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "valueUpdate", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(VariableItemList* input, JsViListContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)variable_item_list_alloc,
.free = (JsViewFree)variable_item_list_free,
.get_view = (JsViewGetView)variable_item_list_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_vi_list_add_child,
.reset_children = (JsViewResetChildren)js_vi_list_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(vi_list, &view_descriptor);

View File

@@ -1,344 +0,0 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/widget.h>
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsWidgetCtx;
#define QUEUE_LEN 2
typedef struct {
GuiButtonType key;
InputType type;
} JsWidgetButtonEvent;
/**
* @brief Parses position (X and Y) from an element declaration object
*/
static bool element_get_position(struct mjs* mjs, mjs_val_t element, int32_t* x, int32_t* y) {
mjs_val_t x_in = mjs_get(mjs, element, "x", ~0);
mjs_val_t y_in = mjs_get(mjs, element, "y", ~0);
if(!mjs_is_number(x_in) || !mjs_is_number(y_in)) return false;
*x = mjs_get_int32(mjs, x_in);
*y = mjs_get_int32(mjs, y_in);
return true;
}
/**
* @brief Parses size (W and h) from an element declaration object
*/
static bool element_get_size(struct mjs* mjs, mjs_val_t element, int32_t* w, int32_t* h) {
mjs_val_t w_in = mjs_get(mjs, element, "w", ~0);
mjs_val_t h_in = mjs_get(mjs, element, "h", ~0);
if(!mjs_is_number(w_in) || !mjs_is_number(h_in)) return false;
*w = mjs_get_int32(mjs, w_in);
*h = mjs_get_int32(mjs, h_in);
return true;
}
/**
* @brief Parses alignment (V and H) from an element declaration object
*/
static bool
element_get_alignment(struct mjs* mjs, mjs_val_t element, Align* align_v, Align* align_h) {
mjs_val_t align_in = mjs_get(mjs, element, "align", ~0);
const char* align = mjs_get_string(mjs, &align_in, NULL);
if(!align) return false;
if(strlen(align) != 2) return false;
if(align[0] == 't') {
*align_v = AlignTop;
} else if(align[0] == 'c') {
*align_v = AlignCenter;
} else if(align[0] == 'b') {
*align_v = AlignBottom;
} else {
return false;
}
if(align[1] == 'l') {
*align_h = AlignLeft;
} else if(align[1] == 'm') { // m = middle
*align_h = AlignCenter;
} else if(align[1] == 'r') {
*align_h = AlignRight;
} else {
return false;
}
return true;
}
/**
* @brief Parses font from an element declaration object
*/
static bool element_get_font(struct mjs* mjs, mjs_val_t element, Font* font) {
mjs_val_t font_in = mjs_get(mjs, element, "font", ~0);
const char* font_str = mjs_get_string(mjs, &font_in, NULL);
if(!font_str) return false;
if(strcmp(font_str, "primary") == 0) {
*font = FontPrimary;
} else if(strcmp(font_str, "secondary") == 0) {
*font = FontSecondary;
} else if(strcmp(font_str, "keyboard") == 0) {
*font = FontKeyboard;
} else if(strcmp(font_str, "big_numbers") == 0) {
*font = FontBigNumbers;
} else {
return false;
}
return true;
}
/**
* @brief Parses text from an element declaration object
*/
static bool element_get_text(struct mjs* mjs, mjs_val_t element, mjs_val_t* text) {
*text = mjs_get(mjs, element, "text", ~0);
return mjs_is_string(*text);
}
/**
* @brief Widget button element callback
*/
static void js_widget_button_callback(GuiButtonType result, InputType type, JsWidgetCtx* context) {
JsWidgetButtonEvent event = {
.key = result,
.type = type,
};
furi_check(furi_message_queue_put(context->queue, &event, 0) == FuriStatusOk);
}
#define DESTRUCTURE_OR_RETURN(mjs, child_obj, part, ...) \
if(!element_get_##part(mjs, child_obj, __VA_ARGS__)) \
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element " #part);
static bool js_widget_add_child(
struct mjs* mjs,
Widget* widget,
JsWidgetCtx* context,
mjs_val_t child_obj) {
UNUSED(context);
if(!mjs_is_object(child_obj))
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "child must be an object");
mjs_val_t element_type_term = mjs_get(mjs, child_obj, "element", ~0);
const char* element_type = mjs_get_string(mjs, &element_type_term, NULL);
if(!element_type)
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "child object must have `element` property");
if((strcmp(element_type, "string") == 0) || (strcmp(element_type, "string_multiline") == 0)) {
int32_t x, y;
Align align_v, align_h;
Font font;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
if(strcmp(element_type, "string") == 0) {
widget_add_string_element(
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
} else {
widget_add_string_multiline_element(
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
}
} else if(strcmp(element_type, "text_box") == 0) {
int32_t x, y, w, h;
Align align_v, align_h;
Font font;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
mjs_val_t strip_to_dots_in = mjs_get(mjs, child_obj, "stripToDots", ~0);
if(!mjs_is_boolean(strip_to_dots_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element stripToDots");
bool strip_to_dots = mjs_get_bool(mjs, strip_to_dots_in);
widget_add_text_box_element(
widget, x, y, w, h, align_h, align_v, mjs_get_string(mjs, &text, NULL), strip_to_dots);
} else if(strcmp(element_type, "text_scroll") == 0) {
int32_t x, y, w, h;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
widget_add_text_scroll_element(widget, x, y, w, h, mjs_get_string(mjs, &text, NULL));
} else if(strcmp(element_type, "button") == 0) {
mjs_val_t btn_in = mjs_get(mjs, child_obj, "button", ~0);
const char* btn_name = mjs_get_string(mjs, &btn_in, NULL);
if(!btn_name)
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element button");
GuiButtonType btn_type;
if(strcmp(btn_name, "left") == 0) {
btn_type = GuiButtonTypeLeft;
} else if(strcmp(btn_name, "center") == 0) {
btn_type = GuiButtonTypeCenter;
} else if(strcmp(btn_name, "right") == 0) {
btn_type = GuiButtonTypeRight;
} else {
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "incorrect button type");
}
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
widget_add_button_element(
widget,
btn_type,
mjs_get_string(mjs, &text, NULL),
(ButtonCallback)js_widget_button_callback,
context);
} else if(strcmp(element_type, "icon") == 0) {
int32_t x, y;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
mjs_val_t icon_data_in = mjs_get(mjs, child_obj, "iconData", ~0);
if(!mjs_is_foreign(icon_data_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element iconData");
const Icon* icon = mjs_get_ptr(mjs, icon_data_in);
widget_add_icon_element(widget, x, y, icon);
} else if(strcmp(element_type, "rect") == 0) {
int32_t x, y, w, h;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
if(!mjs_is_number(radius_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
int32_t radius = mjs_get_int32(mjs, radius_in);
mjs_val_t fill_in = mjs_get(mjs, child_obj, "fill", ~0);
if(!mjs_is_boolean(fill_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element fill");
int32_t fill = mjs_get_bool(mjs, fill_in);
widget_add_rect_element(widget, x, y, w, h, radius, fill);
} else if(strcmp(element_type, "circle") == 0) {
int32_t x, y;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
if(!mjs_is_number(radius_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
int32_t radius = mjs_get_int32(mjs, radius_in);
mjs_val_t fill_in = mjs_get(mjs, child_obj, "fill", ~0);
if(!mjs_is_boolean(fill_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element fill");
int32_t fill = mjs_get_bool(mjs, fill_in);
widget_add_circle_element(widget, x, y, radius, fill);
} else if(strcmp(element_type, "line") == 0) {
int32_t x1, y1, x2, y2;
mjs_val_t x1_in = mjs_get(mjs, child_obj, "x1", ~0);
mjs_val_t y1_in = mjs_get(mjs, child_obj, "y1", ~0);
mjs_val_t x2_in = mjs_get(mjs, child_obj, "x2", ~0);
mjs_val_t y2_in = mjs_get(mjs, child_obj, "y2", ~0);
if(!mjs_is_number(x1_in) || !mjs_is_number(y1_in) || !mjs_is_number(x2_in) ||
!mjs_is_number(y2_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element positions");
x1 = mjs_get_int32(mjs, x1_in);
y1 = mjs_get_int32(mjs, y1_in);
x2 = mjs_get_int32(mjs, x2_in);
y2 = mjs_get_int32(mjs, y2_in);
widget_add_line_element(widget, x1, y1, x2, y2);
}
return true;
}
static void js_widget_reset_children(Widget* widget, void* state) {
UNUSED(state);
widget_reset(widget);
}
static mjs_val_t js_widget_button_event_transformer(
struct mjs* mjs,
FuriMessageQueue* queue,
JsWidgetCtx* context) {
UNUSED(context);
JsWidgetButtonEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
const char* event_key;
if(event.key == GuiButtonTypeLeft) {
event_key = "left";
} else if(event.key == GuiButtonTypeCenter) {
event_key = "center";
} else if(event.key == GuiButtonTypeRight) {
event_key = "right";
} else {
furi_crash();
}
const char* event_type;
if(event.type == InputTypePress) {
event_type = "press";
} else if(event.type == InputTypeRelease) {
event_type = "release";
} else if(event.type == InputTypeShort) {
event_type = "short";
} else if(event.type == InputTypeLong) {
event_type = "long";
} else if(event.type == InputTypeRepeat) {
event_type = "repeat";
} else {
furi_crash();
}
mjs_val_t obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, obj) {
JS_FIELD("key", mjs_mk_string(mjs, event_key, ~0, true));
JS_FIELD("type", mjs_mk_string(mjs, event_type, ~0, true));
}
return obj;
}
static void* js_widget_custom_make(struct mjs* mjs, Widget* widget, mjs_val_t view_obj) {
UNUSED(widget);
JsWidgetCtx* context = malloc(sizeof(JsWidgetCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(JsWidgetButtonEvent));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)js_widget_button_event_transformer,
},
};
mjs_set(mjs, view_obj, "button", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void js_widget_custom_destroy(Widget* widget, JsWidgetCtx* context, FuriEventLoop* loop) {
UNUSED(widget);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)widget_alloc,
.free = (JsViewFree)widget_free,
.get_view = (JsViewGetView)widget_get_view,
.custom_make = (JsViewCustomMake)js_widget_custom_make,
.custom_destroy = (JsViewCustomDestroy)js_widget_custom_destroy,
.add_child = (JsViewAddChild)js_widget_add_child,
.reset_children = (JsViewResetChildren)js_widget_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(widget, &view_descriptor);

View File

@@ -1,280 +0,0 @@
#include "../js_modules.h"
#include <furi_hal_i2c.h>
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_i2c_is_device_ready(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 1, 2)) return;
mjs_val_t addr_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(addr_arg)) {
ret_bad_args(mjs, "Addr must be a number");
return;
}
uint32_t addr = mjs_get_int32(mjs, addr_arg);
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");
return;
}
timeout = mjs_get_int32(mjs, timeout_arg);
}
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool ready = furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external, addr, timeout);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
mjs_return(mjs, mjs_mk_boolean(mjs, ready));
}
static void js_i2c_write(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 2, 3)) return;
mjs_val_t addr_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(addr_arg)) {
ret_bad_args(mjs, "Addr must be a number");
return;
}
uint32_t addr = mjs_get_int32(mjs, addr_arg);
mjs_val_t tx_buf_arg = mjs_arg(mjs, 1);
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) > 2) { // Timeout is optional argument
mjs_val_t timeout_arg = mjs_arg(mjs, 2);
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);
}
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool result = furi_hal_i2c_tx(&furi_hal_i2c_handle_external, addr, tx_buf, tx_len, timeout);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
if(tx_buf_was_allocated) free(tx_buf);
mjs_return(mjs, mjs_mk_boolean(mjs, result));
}
static void js_i2c_read(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 2, 3)) return;
mjs_val_t addr_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(addr_arg)) {
ret_bad_args(mjs, "Addr must be a number");
return;
}
uint32_t addr = mjs_get_int32(mjs, addr_arg);
mjs_val_t rx_len_arg = mjs_arg(mjs, 1);
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) > 2) { // Timeout is optional argument
mjs_val_t timeout_arg = mjs_arg(mjs, 2);
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);
}
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool result = furi_hal_i2c_rx(&furi_hal_i2c_handle_external, addr, rx_buf, rx_len, timeout);
furi_hal_i2c_release(&furi_hal_i2c_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_i2c_write_read(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 3, 4)) return;
mjs_val_t addr_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(addr_arg)) {
ret_bad_args(mjs, "Addr must be a number");
return;
}
uint32_t addr = mjs_get_int32(mjs, addr_arg);
mjs_val_t tx_buf_arg = mjs_arg(mjs, 1);
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;
}
mjs_val_t rx_len_arg = mjs_arg(mjs, 2);
if(!mjs_is_number(rx_len_arg)) {
ret_bad_args(mjs, "Length must be a number");
if(tx_buf_was_allocated) free(tx_buf);
return;
}
size_t rx_len = mjs_get_int32(mjs, rx_len_arg);
if(rx_len == 0) {
ret_bad_args(mjs, "Length must not zero");
if(tx_buf_was_allocated) free(tx_buf);
return;
}
uint8_t* rx_buf = malloc(rx_len);
uint32_t timeout = 1;
if(mjs_nargs(mjs) > 3) { // Timeout is optional argument
mjs_val_t timeout_arg = mjs_arg(mjs, 3);
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);
}
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool result = furi_hal_i2c_trx(
&furi_hal_i2c_handle_external, addr, tx_buf, tx_len, rx_buf, rx_len, timeout);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
mjs_val_t ret = MJS_UNDEFINED;
if(result) {
ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len);
}
if(tx_buf_was_allocated) free(tx_buf);
free(rx_buf);
mjs_return(mjs, ret);
}
static void* js_i2c_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t i2c_obj = mjs_mk_object(mjs);
mjs_set(mjs, i2c_obj, "isDeviceReady", ~0, MJS_MK_FN(js_i2c_is_device_ready));
mjs_set(mjs, i2c_obj, "write", ~0, MJS_MK_FN(js_i2c_write));
mjs_set(mjs, i2c_obj, "read", ~0, MJS_MK_FN(js_i2c_read));
mjs_set(mjs, i2c_obj, "writeRead", ~0, MJS_MK_FN(js_i2c_write_read));
*object = i2c_obj;
return (void*)1;
}
static const JsModuleDescriptor js_i2c_desc = {
"i2c",
js_i2c_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor i2c_plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_i2c_desc,
};
const FlipperAppPluginDescriptor* js_i2c_ep(void) {
return &i2c_plugin_descriptor;
}

View File

@@ -1,129 +0,0 @@
#include "../../js_modules.h"
#include <infrared_worker.h>
#include <lib/infrared/signal/infrared_signal.h>
#include <lib/infrared/worker/infrared_transmit.h>
#define TAG "JsMath"
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);
}
void js_send_protocol_signal(struct mjs* mjs) {
size_t num_args = mjs_nargs(mjs);
if(num_args < 3 || num_args > 4) {
ret_bad_args(mjs, "Wrong argument count");
return;
}
if(!mjs_is_string(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1)) ||
!mjs_is_number(mjs_arg(mjs, 2)) || (num_args == 4 && !mjs_is_object(mjs_arg(mjs, 3)))) {
ret_bad_args(mjs, "Wrong argument type");
return;
}
bool repeat = false;
int times = 1;
if(num_args == 4) {
mjs_val_t options_obj = mjs_arg(mjs, 3);
mjs_val_t repeat_val = mjs_get(mjs, options_obj, "repeat", ~0);
if(mjs_is_boolean(repeat_val)) {
repeat = mjs_get_bool(mjs, repeat_val);
} else if(!mjs_is_undefined(repeat_val)) {
ret_bad_args(mjs, "Wrong 'repeat' option type");
return;
}
mjs_val_t times_val = mjs_get(mjs, options_obj, "times", ~0);
if(mjs_is_number(times_val)) {
times = mjs_get_int(mjs, times_val);
} else if(!mjs_is_undefined(times_val)) {
ret_bad_args(mjs, "Wrong 'times' option type");
return;
}
}
InfraredMessage message;
message.repeat = repeat;
mjs_val_t protocol_arg = mjs_arg(mjs, 0);
message.protocol = infrared_get_protocol_by_name(mjs_get_cstring(mjs, &protocol_arg));
message.address = mjs_get_int(mjs, mjs_arg(mjs, 1));
message.command = mjs_get_int(mjs, mjs_arg(mjs, 2));
infrared_send(&message, times);
}
void js_send_raw_signal(struct mjs* mjs) {
size_t num_args = mjs_nargs(mjs);
if(num_args < 1 || num_args > 3) {
ret_bad_args(mjs, "Wrong argument count");
return;
}
if(!mjs_is_array(mjs_arg(mjs, 0)) || (num_args > 1 && !mjs_is_boolean(mjs_arg(mjs, 1))) ||
(num_args > 2 && !mjs_is_object(mjs_arg(mjs, 2)))) {
ret_bad_args(mjs, "Wrong argument type");
return;
}
int array_length = mjs_array_length(mjs, mjs_arg(mjs, 0));
uint32_t timings[array_length];
for(int i = 0; i < array_length; i++) {
mjs_val_t elem = mjs_array_get(mjs, mjs_arg(mjs, 0), i);
if(!mjs_is_number(elem)) {
ret_bad_args(mjs, "Timings array must contain only numbers");
return;
}
timings[i] = mjs_get_int(mjs, elem);
}
bool start_from_mark = true;
if(num_args > 1) {
start_from_mark = mjs_get_bool(mjs, mjs_arg(mjs, 1));
}
if(num_args > 2) {
mjs_val_t options_obj = mjs_arg(mjs, 2);
mjs_val_t frequency_val = mjs_get(mjs, options_obj, "frequency", ~0);
if(!mjs_is_number(frequency_val)) {
ret_bad_args(mjs, "Wrong 'frequency' option type");
return;
}
mjs_val_t duty_val = mjs_get(mjs, options_obj, "dutyCycle", ~0);
if(!mjs_is_number(duty_val)) {
ret_bad_args(mjs, "Wrong 'dutyCycle' option type");
return;
}
uint32_t frequency = mjs_get_int(mjs, frequency_val);
float duty_cycle = mjs_get_double(mjs, duty_val);
infrared_send_raw_ext(timings, array_length, start_from_mark, frequency, duty_cycle);
} else {
infrared_send_raw(timings, array_length, start_from_mark);
}
return;
}
static void* js_infrared_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t infrared_object = mjs_mk_object(mjs);
mjs_set(mjs, infrared_object, "sendSignal", ~0, MJS_MK_FN(js_send_protocol_signal));
mjs_set(mjs, infrared_object, "sendRawSignal", ~0, MJS_MK_FN(js_send_raw_signal));
*object = infrared_object;
return (void*)1;
}
static const JsModuleDescriptor js_infrared_desc = {
"infrared",
js_infrared_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_infrared_desc,
};
const FlipperAppPluginDescriptor* js_infrared_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,357 +0,0 @@
#include "../js_modules.h"
#include "furi_hal_random.h"
#include <float.h>
#define JS_MATH_PI ((double)M_PI)
#define JS_MATH_E ((double)M_E)
#define JS_MATH_EPSILON ((double)DBL_EPSILON)
#define TAG "JsMath"
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_args(struct mjs* mjs, size_t count) {
size_t num_args = mjs_nargs(mjs);
if(num_args != count) {
ret_bad_args(mjs, "Wrong argument count");
return false;
}
for(size_t i = 0; i < count; i++) {
if(!mjs_is_number(mjs_arg(mjs, i))) {
ret_bad_args(mjs, "Wrong argument type");
return false;
}
}
return true;
}
void js_math_is_equal(struct mjs* mjs) {
if(!check_args(mjs, 3)) {
return;
}
double a = mjs_get_double(mjs, mjs_arg(mjs, 0));
double b = mjs_get_double(mjs, mjs_arg(mjs, 1));
double e = mjs_get_double(mjs, mjs_arg(mjs, 2));
double f = fabs(a - b);
mjs_return(mjs, mjs_mk_boolean(mjs, (f <= e)));
}
void js_math_abs(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, fabs(x)));
}
void js_math_acos(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
if(x < (double)-1. || x > (double)1.) {
ret_bad_args(mjs, "Invalid input value for math.acos");
return;
}
mjs_return(mjs, mjs_mk_number(mjs, acos(x)));
}
void js_math_acosh(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
if(x < (double)1.) {
ret_bad_args(mjs, "Invalid input value for math.acosh");
return;
}
mjs_return(mjs, mjs_mk_number(mjs, log(x + sqrt(x * x - (double)1.))));
}
void js_math_asin(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, asin(x)));
}
void js_math_asinh(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, log(x + sqrt(x * x + (double)1.))));
}
void js_math_atan(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, atan(x)));
}
void js_math_atan2(struct mjs* mjs) {
if(!check_args(mjs, 2)) {
return;
}
double y = mjs_get_double(mjs, mjs_arg(mjs, 0));
double x = mjs_get_double(mjs, mjs_arg(mjs, 1));
mjs_return(mjs, mjs_mk_number(mjs, atan2(y, x)));
}
void js_math_atanh(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
if(x < (double)-1. || x > (double)1.) {
ret_bad_args(mjs, "Invalid input value for math.atanh");
return;
}
mjs_return(mjs, mjs_mk_number(mjs, (double)0.5 * log(((double)1. + x) / ((double)1. - x))));
}
void js_math_cbrt(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, cbrt(x)));
}
void js_math_ceil(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, ceil(x)));
}
void js_math_clz32(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
unsigned int x = (unsigned int)mjs_get_int(mjs, mjs_arg(mjs, 0));
int count = 0;
while(x) {
x >>= 1;
count++;
}
mjs_return(mjs, mjs_mk_number(mjs, 32 - count));
}
void js_math_cos(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, cos(x)));
}
void js_math_exp(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, exp(x)));
}
void js_math_floor(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, floor(x)));
}
void js_math_log(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
if(x <= 0) {
ret_bad_args(mjs, "Invalid input value for math.log");
return;
}
mjs_return(mjs, mjs_mk_number(mjs, log(x)));
}
void js_math_max(struct mjs* mjs) {
if(!check_args(mjs, 2)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
double y = mjs_get_double(mjs, mjs_arg(mjs, 1));
mjs_return(mjs, mjs_mk_number(mjs, x > y ? x : y));
}
void js_math_min(struct mjs* mjs) {
if(!check_args(mjs, 2)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
double y = mjs_get_double(mjs, mjs_arg(mjs, 1));
mjs_return(mjs, mjs_mk_number(mjs, x < y ? x : y));
}
void js_math_pow(struct mjs* mjs) {
if(!check_args(mjs, 2)) {
return;
}
double base = mjs_get_double(mjs, mjs_arg(mjs, 0));
double exponent = mjs_get_double(mjs, mjs_arg(mjs, 1));
mjs_return(mjs, mjs_mk_number(mjs, pow(base, exponent)));
}
void js_math_random(struct mjs* mjs) {
if(!check_args(mjs, 0)) {
return;
}
// double clearly provides more bits for entropy then we pack
// 32bit should be enough for now, but fix it maybe
const uint32_t random_val = furi_hal_random_get();
double rnd = (double)random_val / (double)FURI_HAL_RANDOM_MAX;
mjs_return(mjs, mjs_mk_number(mjs, rnd));
}
void js_math_sign(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(
mjs,
mjs_mk_number(
mjs, fabs(x) <= JS_MATH_EPSILON ? 0 : (x < (double)0. ? (double)-1.0 : (double)1.0)));
}
void js_math_sin(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, sin(x)));
}
void js_math_sqrt(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
if(x < (double)0.) {
ret_bad_args(mjs, "Invalid input value for math.sqrt");
return;
}
mjs_return(mjs, mjs_mk_number(mjs, sqrt(x)));
}
void js_math_trunc(struct mjs* mjs) {
if(!check_args(mjs, 1)) {
return;
}
double x = mjs_get_double(mjs, mjs_arg(mjs, 0));
mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x)));
}
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, "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));
mjs_set(mjs, math_obj, "asin", ~0, MJS_MK_FN(js_math_asin));
mjs_set(mjs, math_obj, "asinh", ~0, MJS_MK_FN(js_math_asinh));
mjs_set(mjs, math_obj, "atan", ~0, MJS_MK_FN(js_math_atan));
mjs_set(mjs, math_obj, "atan2", ~0, MJS_MK_FN(js_math_atan2));
mjs_set(mjs, math_obj, "atanh", ~0, MJS_MK_FN(js_math_atanh));
mjs_set(mjs, math_obj, "cbrt", ~0, MJS_MK_FN(js_math_cbrt));
mjs_set(mjs, math_obj, "ceil", ~0, MJS_MK_FN(js_math_ceil));
mjs_set(mjs, math_obj, "clz32", ~0, MJS_MK_FN(js_math_clz32));
mjs_set(mjs, math_obj, "cos", ~0, MJS_MK_FN(js_math_cos));
mjs_set(mjs, math_obj, "exp", ~0, MJS_MK_FN(js_math_exp));
mjs_set(mjs, math_obj, "floor", ~0, MJS_MK_FN(js_math_floor));
mjs_set(mjs, math_obj, "log", ~0, MJS_MK_FN(js_math_log));
mjs_set(mjs, math_obj, "max", ~0, MJS_MK_FN(js_math_max));
mjs_set(mjs, math_obj, "min", ~0, MJS_MK_FN(js_math_min));
mjs_set(mjs, math_obj, "pow", ~0, MJS_MK_FN(js_math_pow));
mjs_set(mjs, math_obj, "random", ~0, MJS_MK_FN(js_math_random));
mjs_set(mjs, math_obj, "sign", ~0, MJS_MK_FN(js_math_sign));
mjs_set(mjs, math_obj, "sin", ~0, MJS_MK_FN(js_math_sin));
mjs_set(mjs, math_obj, "sqrt", ~0, MJS_MK_FN(js_math_sqrt));
mjs_set(mjs, math_obj, "trunc", ~0, MJS_MK_FN(js_math_trunc));
mjs_set(mjs, math_obj, "PI", ~0, mjs_mk_number(mjs, JS_MATH_PI));
mjs_set(mjs, math_obj, "E", ~0, mjs_mk_number(mjs, JS_MATH_E));
mjs_set(mjs, math_obj, "EPSILON", ~0, mjs_mk_number(mjs, JS_MATH_EPSILON));
*object = math_obj;
return (void*)1;
}
static const JsModuleDescriptor js_math_desc = {
"math",
js_math_create,
NULL,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_math_desc,
};
const FlipperAppPluginDescriptor* js_math_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,111 +0,0 @@
#include <core/common_defines.h>
#include "../js_modules.h"
#include <notification/notification_messages.h>
static void js_notify(struct mjs* mjs, const NotificationSequence* sequence) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
NotificationApp* notification = mjs_get_ptr(mjs, obj_inst);
furi_assert(notification);
notification_message(notification, sequence);
}
static void js_notify_success(struct mjs* mjs) {
js_notify(mjs, &sequence_success);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_notify_error(struct mjs* mjs) {
js_notify(mjs, &sequence_error);
mjs_return(mjs, MJS_UNDEFINED);
}
static const struct {
const char* color_name;
const NotificationSequence* sequence_short;
const NotificationSequence* sequence_long;
} led_sequences[] = {
{"blue", &sequence_blink_blue_10, &sequence_blink_blue_100},
{"red", &sequence_blink_red_10, &sequence_blink_red_100},
{"green", &sequence_blink_green_10, &sequence_blink_green_100},
{"yellow", &sequence_blink_yellow_10, &sequence_blink_yellow_100},
{"cyan", &sequence_blink_cyan_10, &sequence_blink_cyan_100},
{"magenta", &sequence_blink_magenta_10, &sequence_blink_magenta_100},
};
static void js_notify_blink(struct mjs* mjs) {
const NotificationSequence* sequence = NULL;
do {
size_t num_args = mjs_nargs(mjs);
if(num_args != 2) {
break;
}
mjs_val_t color_obj = mjs_arg(mjs, 0);
mjs_val_t type_obj = mjs_arg(mjs, 1);
if((!mjs_is_string(color_obj)) || (!mjs_is_string(type_obj))) break;
size_t arg_len = 0;
const char* arg_str = mjs_get_string(mjs, &color_obj, &arg_len);
if((arg_len == 0) || (arg_str == NULL)) break;
int32_t color_id = -1;
for(size_t i = 0; i < COUNT_OF(led_sequences); i++) {
size_t name_len = strlen(led_sequences[i].color_name);
if(arg_len != name_len) continue;
if(strncmp(arg_str, led_sequences[i].color_name, arg_len) == 0) {
color_id = i;
break;
}
}
if(color_id == -1) break;
arg_str = mjs_get_string(mjs, &type_obj, &arg_len);
if((arg_len == 0) || (arg_str == NULL)) break;
if(strncmp(arg_str, "short", arg_len) == 0) {
sequence = led_sequences[color_id].sequence_short;
} else if(strncmp(arg_str, "long", arg_len) == 0) {
sequence = led_sequences[color_id].sequence_long;
}
} while(0);
if(sequence == NULL) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
} else {
js_notify(mjs, sequence);
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
mjs_val_t notify_obj = mjs_mk_object(mjs);
mjs_set(mjs, notify_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, notification));
mjs_set(mjs, notify_obj, "success", ~0, MJS_MK_FN(js_notify_success));
mjs_set(mjs, notify_obj, "error", ~0, MJS_MK_FN(js_notify_error));
mjs_set(mjs, notify_obj, "blink", ~0, MJS_MK_FN(js_notify_blink));
*object = notify_obj;
return notification;
}
static void js_notification_destroy(void* inst) {
UNUSED(inst);
furi_record_close(RECORD_NOTIFICATION);
}
static const JsModuleDescriptor js_notification_desc = {
"notification",
js_notification_create,
js_notification_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_notification_desc,
};
const FlipperAppPluginDescriptor* js_notification_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,621 +0,0 @@
#include <core/common_defines.h>
#include <expansion/expansion.h>
#include <furi_hal.h>
#include "../js_modules.h"
#include <m-array.h>
#define TAG "JsSerial"
#define RX_BUF_LEN 2048
typedef struct {
bool setup_done;
FuriStreamBuffer* rx_stream;
FuriHalSerialHandle* serial_handle;
struct mjs* mjs;
} JsSerialInst;
typedef struct {
size_t len;
char* data;
} PatternArrayItem;
ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST); //-V658
static void
js_serial_on_async_rx(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) {
JsSerialInst* serial = context;
furi_assert(serial);
if(event & FuriHalSerialRxEventData) {
uint8_t data = furi_hal_serial_async_rx(handle);
furi_stream_buffer_send(serial->rx_stream, &data, 1, 0);
js_flags_set(serial->mjs, ThreadEventCustomDataRx);
}
}
static void js_serial_setup(struct mjs* mjs) {
static const JsValueEnumVariant js_serial_id_variants[] = {
{"lpuart", FuriHalSerialIdLpuart},
{"usart", FuriHalSerialIdUsart},
};
static const JsValueEnumVariant js_serial_data_bit_variants[] = {
{"6", FuriHalSerialDataBits6},
{"7", FuriHalSerialDataBits7},
{"8", FuriHalSerialDataBits8},
{"9", FuriHalSerialDataBits9},
};
static const JsValueDeclaration js_serial_data_bits = JS_VALUE_ENUM_W_DEFAULT(
FuriHalSerialDataBits, js_serial_data_bit_variants, FuriHalSerialDataBits8);
static const JsValueEnumVariant js_serial_parity_variants[] = {
{"none", FuriHalSerialParityNone},
{"even", FuriHalSerialParityEven},
{"odd", FuriHalSerialParityOdd},
};
static const JsValueDeclaration js_serial_parity = JS_VALUE_ENUM_W_DEFAULT(
FuriHalSerialParity, js_serial_parity_variants, FuriHalSerialParityNone);
static const JsValueEnumVariant js_serial_stop_bit_variants[] = {
{"0.5", FuriHalSerialStopBits0_5},
{"1", FuriHalSerialStopBits1},
{"1.5", FuriHalSerialStopBits1_5},
{"2", FuriHalSerialStopBits2},
};
static const JsValueDeclaration js_serial_stop_bits = JS_VALUE_ENUM_W_DEFAULT(
FuriHalSerialStopBits, js_serial_stop_bit_variants, FuriHalSerialStopBits1);
static const JsValueObjectField js_serial_framing_fields[] = {
{"dataBits", &js_serial_data_bits},
{"parity", &js_serial_parity},
{"stopBits", &js_serial_stop_bits},
};
static const JsValueDeclaration js_serial_setup_arg_list[] = {
JS_VALUE_ENUM(FuriHalSerialId, js_serial_id_variants),
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_OBJECT_W_DEFAULTS(js_serial_framing_fields),
};
static const JsValueArguments js_serial_setup_args = JS_VALUE_ARGS(js_serial_setup_arg_list);
FuriHalSerialId serial_id;
int32_t baudrate;
FuriHalSerialDataBits data_bits = FuriHalSerialDataBits8;
FuriHalSerialParity parity = FuriHalSerialParityNone;
FuriHalSerialStopBits stop_bits = FuriHalSerialStopBits1;
JS_VALUE_PARSE_ARGS_OR_RETURN(
mjs, &js_serial_setup_args, &serial_id, &baudrate, &data_bits, &parity, &stop_bits);
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
if(serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is already configured");
expansion_disable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
serial->serial_handle = furi_hal_serial_control_acquire(serial_id);
if(serial->serial_handle) {
serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1);
furi_hal_serial_init(serial->serial_handle, baudrate);
furi_hal_serial_configure_framing(serial->serial_handle, data_bits, parity, stop_bits);
furi_hal_serial_async_rx_start(
serial->serial_handle, js_serial_on_async_rx, serial, false);
serial->setup_done = true;
} else {
expansion_enable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
}
}
static void js_serial_deinit(JsSerialInst* js_serial) {
if(js_serial->setup_done) {
furi_hal_serial_async_rx_stop(js_serial->serial_handle);
furi_hal_serial_deinit(js_serial->serial_handle);
furi_hal_serial_control_release(js_serial->serial_handle);
js_serial->serial_handle = NULL;
furi_stream_buffer_free(js_serial->rx_stream);
expansion_enable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
js_serial->setup_done = false;
}
}
static void js_serial_end(struct mjs* mjs) {
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
furi_assert(serial);
if(!serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
js_serial_deinit(serial);
}
static void js_serial_write(struct mjs* mjs) {
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
furi_assert(serial);
if(!serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
bool args_correct = true;
size_t num_args = mjs_nargs(mjs);
for(size_t i = 0; i < num_args; i++) {
mjs_val_t arg = mjs_arg(mjs, i);
if(mjs_is_string(arg)) {
size_t str_len = 0;
const char* arg_str = mjs_get_string(mjs, &arg, &str_len);
if((str_len == 0) || (arg_str == NULL)) {
args_correct = false;
break;
}
furi_hal_serial_tx(serial->serial_handle, (uint8_t*)arg_str, str_len);
} else if(mjs_is_number(arg)) {
uint32_t byte_val = mjs_get_int32(mjs, arg);
if(byte_val > 0xFF) {
args_correct = false;
break;
}
furi_hal_serial_tx(serial->serial_handle, (uint8_t*)&byte_val, 1);
} else if(mjs_is_array(arg)) {
size_t array_len = mjs_array_length(mjs, arg);
for(size_t i = 0; i < array_len; i++) {
mjs_val_t array_arg = mjs_array_get(mjs, arg, i);
if(!mjs_is_number(array_arg)) {
args_correct = false;
break;
}
uint32_t byte_val = mjs_get_int32(mjs, array_arg);
if(byte_val > 0xFF) {
args_correct = false;
break;
}
furi_hal_serial_tx(serial->serial_handle, (uint8_t*)&byte_val, 1);
}
if(!args_correct) {
break;
}
} else if(mjs_is_typed_array(arg)) {
mjs_val_t array_buf = arg;
if(mjs_is_data_view(arg)) {
array_buf = mjs_dataview_get_buf(mjs, arg);
}
size_t len = 0;
char* buf = mjs_array_buf_get_ptr(mjs, array_buf, &len);
furi_hal_serial_tx(serial->serial_handle, (uint8_t*)buf, len);
} else {
args_correct = false;
break;
}
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
}
mjs_return(mjs, MJS_UNDEFINED);
}
static size_t js_serial_receive(JsSerialInst* serial, char* buf, size_t len, uint32_t timeout) {
size_t bytes_read = 0;
while(1) {
uint32_t flags = ThreadEventCustomDataRx;
if(furi_stream_buffer_is_empty(serial->rx_stream)) {
flags = js_flags_wait(serial->mjs, ThreadEventCustomDataRx, timeout);
}
if(flags == 0) { // Timeout
break;
} else if(flags & ThreadEventStop) { // Exit flag
bytes_read = 0;
break;
} else if(flags & ThreadEventCustomDataRx) { // New data received
size_t rx_len = furi_stream_buffer_receive(
serial->rx_stream, &buf[bytes_read], len - bytes_read, 0);
bytes_read += rx_len;
if(bytes_read == len) {
break;
}
}
}
return bytes_read;
}
static const JsValueDeclaration js_serial_read_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, INT32_MAX),
};
static const JsValueArguments js_serial_read_args = JS_VALUE_ARGS(js_serial_read_arg_list);
static void js_serial_read(struct mjs* mjs) {
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
furi_assert(serial);
if(!serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
int32_t read_len, timeout;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_args, &read_len, &timeout);
char* read_buf = malloc(read_len);
size_t bytes_read = js_serial_receive(serial, read_buf, read_len, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0) {
return_obj = mjs_mk_string(mjs, read_buf, bytes_read, true);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static void js_serial_readln(struct mjs* mjs) {
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
furi_assert(serial);
if(!serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
static const JsValueDeclaration js_serial_readln_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_serial_readln_args = JS_VALUE_ARGS(js_serial_readln_arg_list);
int32_t timeout;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_readln_args, &timeout);
FuriString* rx_buf = furi_string_alloc();
size_t bytes_read = 0;
char read_char = 0;
while(1) {
size_t read_len = js_serial_receive(serial, &read_char, 1, timeout);
if(read_len != 1) {
break;
}
if((read_char == '\r') || (read_char == '\n')) {
break;
} else {
furi_string_push_back(rx_buf, read_char);
bytes_read++;
}
}
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0) {
return_obj = mjs_mk_string(mjs, furi_string_get_cstr(rx_buf), bytes_read, true);
}
mjs_return(mjs, return_obj);
furi_string_free(rx_buf);
}
static void js_serial_read_bytes(struct mjs* mjs) {
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
furi_assert(serial);
if(!serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
int32_t read_len, timeout;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_args, &read_len, &timeout);
char* read_buf = malloc(read_len);
size_t bytes_read = js_serial_receive(serial, read_buf, read_len, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0) {
return_obj = mjs_mk_array_buf(mjs, read_buf, bytes_read);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static char* js_serial_receive_any(JsSerialInst* serial, size_t* len, uint32_t timeout) {
uint32_t flags = ThreadEventCustomDataRx;
if(furi_stream_buffer_is_empty(serial->rx_stream)) {
flags = js_flags_wait(serial->mjs, ThreadEventCustomDataRx, timeout);
}
if(flags & ThreadEventCustomDataRx) { // New data received
*len = furi_stream_buffer_bytes_available(serial->rx_stream);
if(!*len) return NULL;
char* buf = malloc(*len);
furi_stream_buffer_receive(serial->rx_stream, buf, *len, 0);
return buf;
}
return NULL;
}
static void js_serial_read_any(struct mjs* mjs) {
JsSerialInst* serial = JS_GET_CONTEXT(mjs);
furi_assert(serial);
if(!serial->setup_done)
JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
static const JsValueDeclaration js_serial_read_any_arg_list[] = {
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, INT32_MAX),
};
static const JsValueArguments js_serial_read_any_args =
JS_VALUE_ARGS(js_serial_read_any_arg_list);
int32_t timeout;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_any_args, &timeout);
size_t bytes_read = 0;
char* read_buf = js_serial_receive_any(serial, &bytes_read, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0 && read_buf) {
return_obj = mjs_mk_string(mjs, read_buf, bytes_read, true);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static bool
js_serial_expect_parse_string(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) {
size_t str_len = 0;
const char* arg_str = mjs_get_string(mjs, &arg, &str_len);
if((str_len == 0) || (arg_str == NULL)) {
return false;
}
PatternArrayItem* item = PatternArray_push_new(patterns);
item->data = malloc(str_len + 1);
memcpy(item->data, arg_str, str_len);
item->len = str_len;
return true;
}
static bool js_serial_expect_parse_array(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) {
size_t array_len = mjs_array_length(mjs, arg);
if(array_len == 0) {
return false;
}
char* array_data = malloc(array_len + 1);
for(size_t i = 0; i < array_len; i++) {
mjs_val_t array_arg = mjs_array_get(mjs, arg, i);
if(!mjs_is_number(array_arg)) {
free(array_data);
return false;
}
uint32_t byte_val = mjs_get_int32(mjs, array_arg);
if(byte_val > 0xFF) {
free(array_data);
return false;
}
array_data[i] = byte_val;
}
PatternArrayItem* item = PatternArray_push_new(patterns);
item->data = array_data;
item->len = array_len;
return true;
}
static bool
js_serial_expect_parse_args(struct mjs* mjs, PatternArray_t patterns, uint32_t* timeout) {
size_t num_args = mjs_nargs(mjs);
if(num_args == 2) {
mjs_val_t timeout_arg = mjs_arg(mjs, 1);
if(!mjs_is_number(timeout_arg)) {
return false;
}
*timeout = mjs_get_int32(mjs, timeout_arg);
} else if(num_args != 1) {
return false;
}
mjs_val_t patterns_arg = mjs_arg(mjs, 0);
if(mjs_is_string(patterns_arg)) { // Single string pattern
if(!js_serial_expect_parse_string(mjs, patterns_arg, patterns)) {
return false;
}
} else if(mjs_is_array(patterns_arg)) {
size_t array_len = mjs_array_length(mjs, patterns_arg);
if(array_len == 0) {
return false;
}
mjs_val_t array_arg = mjs_array_get(mjs, patterns_arg, 0);
if(mjs_is_number(array_arg)) { // Binary array pattern
if(!js_serial_expect_parse_array(mjs, patterns_arg, patterns)) {
return false;
}
} else if((mjs_is_string(array_arg)) || (mjs_is_array(array_arg))) { // Multiple patterns
for(size_t i = 0; i < array_len; i++) {
mjs_val_t arg = mjs_array_get(mjs, patterns_arg, i);
if(mjs_is_string(arg)) {
if(!js_serial_expect_parse_string(mjs, arg, patterns)) {
return false;
}
} else if(mjs_is_array(arg)) {
if(!js_serial_expect_parse_array(mjs, arg, patterns)) {
return false;
}
}
}
} else {
return false;
}
} else {
return false;
}
return true;
}
static int32_t js_serial_expect_check_pattern_start(
PatternArray_t patterns,
char value,
int32_t pattern_last) {
size_t array_len = PatternArray_size(patterns);
if((pattern_last + 1) >= (int32_t)array_len) {
return -1;
}
for(size_t i = pattern_last + 1; i < array_len; i++) {
if(PatternArray_get(patterns, i)->data[0] == value) {
return i;
}
}
return -1;
}
static void js_serial_expect(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
furi_assert(serial);
if(!serial->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
uint32_t timeout = FuriWaitForever;
PatternArray_t patterns;
PatternArray_it_t it;
PatternArray_init(patterns);
if(!js_serial_expect_parse_args(mjs, patterns, &timeout)) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
for(PatternArray_it(it, patterns); !PatternArray_end_p(it); PatternArray_next(it)) {
const PatternArrayItem* item = PatternArray_cref(it);
free(item->data);
}
PatternArray_clear(patterns);
return;
}
size_t pattern_len_max = 0;
for(PatternArray_it(it, patterns); !PatternArray_end_p(it); PatternArray_next(it)) {
const PatternArrayItem* item = PatternArray_cref(it);
if(item->len > pattern_len_max) {
pattern_len_max = item->len;
}
}
char* compare_buf = malloc(pattern_len_max);
int32_t pattern_found = -1;
int32_t pattern_candidate = -1;
size_t buf_len = 0;
bool is_timeout = false;
while(1) {
if(buf_len == 0) {
// Empty buffer - read by 1 byte to find pattern start
size_t bytes_read = js_serial_receive(serial, &compare_buf[0], 1, timeout);
if(bytes_read != 1) {
is_timeout = true;
break;
}
pattern_candidate = js_serial_expect_check_pattern_start(patterns, compare_buf[0], -1);
if(pattern_candidate == -1) {
continue;
}
buf_len = 1;
}
assert(pattern_candidate >= 0);
// Read next and try to find pattern match
PatternArrayItem* pattern_cur = PatternArray_get(patterns, pattern_candidate);
pattern_found = pattern_candidate;
for(size_t i = 0; i < pattern_cur->len; i++) {
if(i >= buf_len) {
size_t bytes_read = js_serial_receive(serial, &compare_buf[i], 1, timeout);
if(bytes_read != 1) {
is_timeout = true;
break;
}
buf_len++;
}
if(compare_buf[i] != pattern_cur->data[i]) {
pattern_found = -1;
break;
}
}
if((is_timeout) || (pattern_found >= 0)) {
break;
}
// Search other patterns with the same start char
pattern_candidate =
js_serial_expect_check_pattern_start(patterns, compare_buf[0], pattern_candidate);
if(pattern_candidate >= 0) {
continue;
}
// Look for another pattern start
for(size_t i = 1; i < buf_len; i++) {
pattern_candidate = js_serial_expect_check_pattern_start(patterns, compare_buf[i], -1);
if(pattern_candidate >= 0) {
memmove(&compare_buf[0], &compare_buf[i], buf_len - i);
buf_len -= i;
break;
}
}
if(pattern_candidate >= 0) {
continue;
}
// Nothing found - reset buffer
buf_len = 0;
}
if(is_timeout) {
FURI_LOG_W(TAG, "Expect: timeout");
}
for(PatternArray_it(it, patterns); !PatternArray_end_p(it); PatternArray_next(it)) {
const PatternArrayItem* item = PatternArray_cref(it);
free(item->data);
}
PatternArray_clear(patterns);
free(compare_buf);
if(pattern_found >= 0) {
mjs_return(mjs, mjs_mk_number(mjs, pattern_found));
} else {
mjs_return(mjs, MJS_UNDEFINED);
}
}
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsSerialInst* js_serial = malloc(sizeof(JsSerialInst));
js_serial->mjs = mjs;
mjs_val_t serial_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, serial_obj) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, js_serial));
JS_FIELD("setup", MJS_MK_FN(js_serial_setup));
JS_FIELD("end", MJS_MK_FN(js_serial_end));
JS_FIELD("write", MJS_MK_FN(js_serial_write));
JS_FIELD("read", MJS_MK_FN(js_serial_read));
JS_FIELD("readln", MJS_MK_FN(js_serial_readln));
JS_FIELD("readBytes", MJS_MK_FN(js_serial_read_bytes));
JS_FIELD("readAny", MJS_MK_FN(js_serial_read_any));
JS_FIELD("expect", MJS_MK_FN(js_serial_expect));
}
*object = serial_obj;
return js_serial;
}
static void js_serial_destroy(void* inst) {
JsSerialInst* js_serial = inst;
js_serial_deinit(js_serial);
free(js_serial);
}
static const JsModuleDescriptor js_serial_desc = {
"serial",
js_serial_create,
js_serial_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_serial_desc,
};
const FlipperAppPluginDescriptor* js_serial_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,283 +0,0 @@
#include "../js_modules.h"
#include <furi_hal_spi.h>
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;
}

View File

@@ -1,450 +0,0 @@
#include "../js_modules.h" // IWYU pragma: keep
#include <path.h>
// ==========================
// Common argument signatures
// ==========================
static const JsValueDeclaration js_storage_1_int_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_storage_1_int_args = JS_VALUE_ARGS(js_storage_1_int_arg_list);
static const JsValueDeclaration js_storage_1_str_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
};
static const JsValueArguments js_storage_1_str_args = JS_VALUE_ARGS(js_storage_1_str_arg_list);
static const JsValueDeclaration js_storage_2_str_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE(JsValueTypeString),
};
static const JsValueArguments js_storage_2_str_args = JS_VALUE_ARGS(js_storage_2_str_arg_list);
// ======================
// File object operations
// ======================
static void js_storage_file_close(struct mjs* mjs) {
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file)));
}
static void js_storage_file_is_open(struct mjs* mjs) {
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file)));
}
static void js_storage_file_read(struct mjs* mjs) {
typedef enum {
JsStorageReadModeAscii,
JsStorageReadModeBinary,
} JsStorageReadMode;
static const JsValueEnumVariant js_storage_read_mode_variants[] = {
{"ascii", JsStorageReadModeAscii},
{"binary", JsStorageReadModeBinary},
};
static const JsValueDeclaration js_storage_read_arg_list[] = {
JS_VALUE_ENUM(JsStorageReadMode, js_storage_read_mode_variants),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_storage_read_args = JS_VALUE_ARGS(js_storage_read_arg_list);
JsStorageReadMode read_mode;
int32_t length;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_read_args, &read_mode, &length);
File* file = JS_GET_CONTEXT(mjs);
char buffer[length];
size_t actually_read = storage_file_read(file, buffer, length);
if(read_mode == JsStorageReadModeAscii) {
mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true));
} else if(read_mode == JsStorageReadModeBinary) {
mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read));
}
}
static void js_storage_file_write(struct mjs* mjs) {
static const JsValueDeclaration js_storage_file_write_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
};
static const JsValueArguments js_storage_file_write_args =
JS_VALUE_ARGS(js_storage_file_write_arg_list);
mjs_val_t data;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_file_write_args, &data);
const void* buf;
size_t len;
if(mjs_is_string(data)) {
buf = mjs_get_string(mjs, &data, &len);
} else if(mjs_is_array_buf(data)) {
buf = mjs_array_buf_get_ptr(mjs, data, &len);
} else {
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: expected string or ArrayBuffer");
}
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len)));
}
static void js_storage_file_seek_relative(struct mjs* mjs) {
int32_t offset;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_int_args, &offset);
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false)));
}
static void js_storage_file_seek_absolute(struct mjs* mjs) {
int32_t offset;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_int_args, &offset);
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true)));
}
static void js_storage_file_tell(struct mjs* mjs) {
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file)));
}
static void js_storage_file_truncate(struct mjs* mjs) {
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file)));
}
static void js_storage_file_size(struct mjs* mjs) {
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file)));
}
static void js_storage_file_eof(struct mjs* mjs) {
File* file = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file)));
}
static void js_storage_file_copy_to(struct mjs* mjs) {
static const JsValueDeclaration js_storage_file_write_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeAny),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_storage_file_write_args =
JS_VALUE_ARGS(js_storage_file_write_arg_list);
mjs_val_t dest_obj;
int32_t bytes;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_file_write_args, &dest_obj, &bytes);
File* source = JS_GET_CONTEXT(mjs);
File* destination = JS_GET_INST(mjs, dest_obj);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes)));
}
// =========================
// Top-level file operations
// =========================
// common destructor for file and dir objects
static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) {
File* file = JS_GET_INST(mjs, obj);
storage_file_free(file);
}
static void js_storage_open_file(struct mjs* mjs) {
static const JsValueEnumVariant js_storage_fsam_variants[] = {
{"r", FSAM_READ},
{"w", FSAM_WRITE},
{"rw", FSAM_READ_WRITE},
};
static const JsValueEnumVariant js_storage_fsom_variants[] = {
{"open_existing", FSOM_OPEN_EXISTING},
{"open_always", FSOM_OPEN_ALWAYS},
{"open_append", FSOM_OPEN_APPEND},
{"create_new", FSOM_CREATE_NEW},
{"create_always", FSOM_CREATE_ALWAYS},
};
static const JsValueDeclaration js_storage_open_file_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_ENUM(FS_AccessMode, js_storage_fsam_variants),
JS_VALUE_ENUM(FS_OpenMode, js_storage_fsom_variants),
};
static const JsValueArguments js_storage_open_file_args =
JS_VALUE_ARGS(js_storage_open_file_arg_list);
const char* path;
FS_AccessMode access_mode;
FS_OpenMode open_mode;
JS_VALUE_PARSE_ARGS_OR_RETURN(
mjs, &js_storage_open_file_args, &path, &access_mode, &open_mode);
Storage* storage = JS_GET_CONTEXT(mjs);
File* file = storage_file_alloc(storage);
if(!storage_file_open(file, path, access_mode, open_mode)) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_val_t file_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, file_obj) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, file));
JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_file_destructor));
JS_FIELD("close", MJS_MK_FN(js_storage_file_close));
JS_FIELD("isOpen", MJS_MK_FN(js_storage_file_is_open));
JS_FIELD("read", MJS_MK_FN(js_storage_file_read));
JS_FIELD("write", MJS_MK_FN(js_storage_file_write));
JS_FIELD("seekRelative", MJS_MK_FN(js_storage_file_seek_relative));
JS_FIELD("seekAbsolute", MJS_MK_FN(js_storage_file_seek_absolute));
JS_FIELD("tell", MJS_MK_FN(js_storage_file_tell));
JS_FIELD("truncate", MJS_MK_FN(js_storage_file_truncate));
JS_FIELD("size", MJS_MK_FN(js_storage_file_size));
JS_FIELD("eof", MJS_MK_FN(js_storage_file_eof));
JS_FIELD("copyTo", MJS_MK_FN(js_storage_file_copy_to));
}
mjs_return(mjs, file_obj);
}
static void js_storage_file_exists(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path)));
}
// ====================
// Directory operations
// ====================
static void js_storage_read_directory(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
File* dir = storage_file_alloc(storage);
if(!storage_dir_open(dir, path)) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
FileInfo file_info;
char name[128];
FuriString* file_path = furi_string_alloc_set_str(path);
size_t path_size = furi_string_size(file_path);
uint32_t timestamp;
mjs_val_t ret = mjs_mk_array(mjs);
while(storage_dir_read(dir, &file_info, name, sizeof(name))) {
furi_string_left(file_path, path_size);
path_append(file_path, name);
furi_check(
storage_common_timestamp(storage, furi_string_get_cstr(file_path), &timestamp) ==
FSE_OK);
mjs_val_t obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, obj) {
JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true));
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp));
}
mjs_array_push(mjs, ret, obj);
}
storage_file_free(dir);
furi_string_free(file_path);
mjs_return(mjs, ret);
}
static void js_storage_directory_exists(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path)));
}
static void js_storage_make_directory(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path)));
}
// =================
// Common operations
// =================
static void js_storage_file_or_dir_exists(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path)));
}
static void js_storage_stat(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
FileInfo file_info;
uint32_t timestamp;
if((storage_common_stat(storage, path, &file_info) |
storage_common_timestamp(storage, path, &timestamp)) != FSE_OK) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_val_t ret = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, ret) {
JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1));
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp));
}
mjs_return(mjs, ret);
}
static void js_storage_remove(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path)));
}
static void js_storage_rmrf(struct mjs* mjs) {
const char* path;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path)));
}
static void js_storage_rename(struct mjs* mjs) {
const char *old, *new;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &old, &new);
Storage* storage = JS_GET_CONTEXT(mjs);
FS_Error status = storage_common_rename(storage, old, new);
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK));
}
static void js_storage_copy(struct mjs* mjs) {
const char *source, *dest;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &source, &dest);
Storage* storage = JS_GET_CONTEXT(mjs);
FS_Error status = storage_common_copy(storage, source, dest);
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST));
}
static void js_storage_fs_info(struct mjs* mjs) {
const char* fs;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &fs);
Storage* storage = JS_GET_CONTEXT(mjs);
uint64_t total_space, free_space;
if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_val_t ret = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, ret) {
JS_FIELD("totalSpace", mjs_mk_number(mjs, total_space));
JS_FIELD("freeSpace", mjs_mk_number(mjs, free_space));
}
mjs_return(mjs, ret);
}
static void js_storage_next_available_filename(struct mjs* mjs) {
static const JsValueDeclaration js_storage_naf_arg_list[] = {
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE(JsValueTypeString),
JS_VALUE_SIMPLE(JsValueTypeInt32),
};
static const JsValueArguments js_storage_naf_args = JS_VALUE_ARGS(js_storage_naf_arg_list);
const char *dir_path, *file_name, *file_ext;
int32_t max_len;
JS_VALUE_PARSE_ARGS_OR_RETURN(
mjs, &js_storage_naf_args, &dir_path, &file_name, &file_ext, &max_len);
Storage* storage = JS_GET_CONTEXT(mjs);
FuriString* next_name = furi_string_alloc();
storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len);
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(next_name), ~0, true));
furi_string_free(next_name);
}
// ===============
// Path operations
// ===============
static void js_storage_are_paths_equal(struct mjs* mjs) {
const char *path1, *path2;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &path1, &path2);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2)));
}
static void js_storage_is_subpath_of(struct mjs* mjs) {
const char *parent, *child;
JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &parent, &child);
Storage* storage = JS_GET_CONTEXT(mjs);
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child)));
}
// ==================
// Module ctor & dtor
// ==================
static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
Storage* storage = furi_record_open(RECORD_STORAGE);
UNUSED(storage);
*object = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, *object) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, storage));
// top-level file ops
JS_FIELD("openFile", MJS_MK_FN(js_storage_open_file));
JS_FIELD("fileExists", MJS_MK_FN(js_storage_file_exists));
// dir ops
JS_FIELD("readDirectory", MJS_MK_FN(js_storage_read_directory));
JS_FIELD("directoryExists", MJS_MK_FN(js_storage_directory_exists));
JS_FIELD("makeDirectory", MJS_MK_FN(js_storage_make_directory));
// common ops
JS_FIELD("fileOrDirExists", MJS_MK_FN(js_storage_file_or_dir_exists));
JS_FIELD("stat", MJS_MK_FN(js_storage_stat));
JS_FIELD("remove", MJS_MK_FN(js_storage_remove));
JS_FIELD("rmrf", MJS_MK_FN(js_storage_rmrf));
JS_FIELD("rename", MJS_MK_FN(js_storage_rename));
JS_FIELD("copy", MJS_MK_FN(js_storage_copy));
JS_FIELD("fsInfo", MJS_MK_FN(js_storage_fs_info));
JS_FIELD("nextAvailableFilename", MJS_MK_FN(js_storage_next_available_filename));
// path ops
JS_FIELD("arePathsEqual", MJS_MK_FN(js_storage_are_paths_equal));
JS_FIELD("isSubpathOf", MJS_MK_FN(js_storage_is_subpath_of));
}
return NULL;
}
static void js_storage_destroy(void* data) {
UNUSED(data);
furi_record_close(RECORD_STORAGE);
}
// ===========
// Boilerplate
// ===========
static const JsModuleDescriptor js_storage_desc = {
"storage",
js_storage_create,
js_storage_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_storage_desc,
};
const FlipperAppPluginDescriptor* js_storage_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,541 +0,0 @@
#include "../../js_modules.h"
#include "radio_device_loader.h"
#include <lib/subghz/transmitter.h>
#include <lib/subghz/devices/devices.h>
#include <lib/subghz/protocols/protocol_items.h>
#include <flipper_format/flipper_format_i.h>
#define TAG "js_subghz"
typedef enum {
JsSubghzRadioStateRX,
JsSubghzRadioStateTX,
JsSubghzRadioStateIDLE,
} JsSubghzRadioState;
typedef struct {
const SubGhzDevice* radio_device;
int frequency;
bool is_external;
JsSubghzRadioState state;
} JsSubghzInst;
static FuriHalSubGhzPreset js_subghz_get_preset_name(const char* preset_name) {
FuriHalSubGhzPreset preset = FuriHalSubGhzPresetIDLE;
if(!strcmp(preset_name, "FuriHalSubGhzPresetOok270Async")) {
preset = FuriHalSubGhzPresetOok270Async;
} else if(!strcmp(preset_name, "FuriHalSubGhzPresetOok650Async")) {
preset = FuriHalSubGhzPresetOok650Async;
} else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev238Async")) {
preset = FuriHalSubGhzPreset2FSKDev238Async;
} else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev12KAsync")) {
preset = FuriHalSubGhzPreset2FSKDev12KAsync;
} else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev476Async")) {
preset = FuriHalSubGhzPreset2FSKDev476Async;
} else if(!strcmp(preset_name, "FuriHalSubGhzPresetCustom")) {
preset = FuriHalSubGhzPresetCustom;
} else {
FURI_LOG_I(TAG, "unknown preset");
}
return preset;
}
static void js_subghz_set_rx(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(js_subghz->state != JsSubghzRadioStateRX) {
subghz_devices_set_rx(js_subghz->radio_device);
js_subghz->state = JsSubghzRadioStateRX;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_subghz_set_idle(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(js_subghz->state != JsSubghzRadioStateIDLE) {
subghz_devices_idle(js_subghz->radio_device);
js_subghz->state = JsSubghzRadioStateIDLE;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_subghz_get_rssi(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(js_subghz->state != JsSubghzRadioStateRX) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
float rssi = subghz_devices_get_rssi(js_subghz->radio_device);
mjs_return(mjs, mjs_mk_number(mjs, (double)rssi));
}
static void js_subghz_get_state(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
const char* state;
switch(js_subghz->state) {
case JsSubghzRadioStateRX:
state = "RX";
break;
case JsSubghzRadioStateTX:
state = "TX";
break;
case JsSubghzRadioStateIDLE:
state = "IDLE";
break;
default:
state = "";
break;
}
mjs_return(mjs, mjs_mk_string(mjs, state, ~0, true));
}
static void js_subghz_is_external(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_return(mjs, mjs_mk_boolean(mjs, js_subghz->is_external));
}
static void js_subghz_set_frequency(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(js_subghz->state != JsSubghzRadioStateIDLE) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not in IDLE state");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_val_t frequency_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(frequency_arg)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Frequency must be a number");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
int32_t frequency = mjs_get_int32(mjs, frequency_arg);
if(!subghz_devices_is_frequency_valid(js_subghz->radio_device, frequency)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Invalid frequency");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_subghz->frequency = subghz_devices_set_frequency(js_subghz->radio_device, frequency);
mjs_return(mjs, mjs_mk_number(mjs, (double)js_subghz->frequency));
}
static void js_subghz_get_frequency(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_return(mjs, mjs_mk_number(mjs, (double)js_subghz->frequency));
}
static void js_subghz_transmit_file(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_val_t file = mjs_arg(mjs, 0);
if(!mjs_is_string(file)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "File must be a string");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
const char* file_path = mjs_get_string(mjs, &file, NULL);
if(!file_path) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to get file path");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
// Repeat works weirdly:
// - "Repeat" in parsed protocol is like holding Send in Sub-GHz app
// This is necessary as most receivers require hearing signals multiple times
// - "repeat" as variable and loop in this code applies to RAW files only
// parsed files handle repeat in protocol layer instead
// We keep 0 as default, or literal value if specified by user
// If user did not specify, 0 is detected below, and we use:
// - 1 repeat for RAW
// - 10 repeats for parsed, which is passed to protocol, and we loop once here
uint32_t repeat = 0;
mjs_val_t repeat_arg = mjs_arg(mjs, 1);
if(mjs_is_number(repeat_arg)) {
int32_t repeat_val = mjs_get_int32(mjs, repeat_arg);
repeat = MAX(repeat_val, 0);
}
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* fff_file = flipper_format_file_alloc(storage);
FlipperFormat* fff_raw = NULL;
if(!flipper_format_file_open_existing(fff_file, file_path)) {
flipper_format_free(fff_file);
furi_record_close(RECORD_STORAGE);
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to open file");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
SubGhzEnvironment* environment = subghz_environment_alloc();
if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME)) {
FURI_LOG_W(TAG, "Load_keystore keeloq_mfcodes - failed to load");
}
if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME)) {
FURI_LOG_W(TAG, "Load_keystore keeloq_mfcodes_user - failed to load");
}
subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
subghz_environment_set_nice_flor_s_rainbow_table_file_name(
environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry);
FuriString* temp_str = furi_string_alloc();
SubGhzTransmitter* transmitter = NULL;
uint32_t temp_data32 = 0;
uint32_t frequency = 0;
bool is_sent = false;
do {
if(!flipper_format_read_header(fff_file, temp_str, &temp_data32)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Missing or incorrect header");
break;
}
if(((!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_KEY_FILE_TYPE)) ||
(!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_RAW_FILE_TYPE))) &&
temp_data32 == SUBGHZ_KEY_FILE_VERSION) {
} else {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Type or version mismatch");
break;
}
if(!flipper_format_read_uint32(fff_file, "Frequency", &frequency, 1)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Missing Frequency");
break;
}
if(!subghz_devices_is_frequency_valid(js_subghz->radio_device, frequency)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Unsupported frequency");
break;
}
if(!flipper_format_read_string(fff_file, "Preset", temp_str)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Missing Preset");
break;
}
FuriHalSubGhzPreset preset = js_subghz_get_preset_name(furi_string_get_cstr(temp_str));
if(preset == FuriHalSubGhzPresetIDLE) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Unknown preset");
break;
}
subghz_devices_reset(js_subghz->radio_device);
subghz_devices_idle(js_subghz->radio_device);
if(preset == FuriHalSubGhzPresetCustom) {
uint8_t* custom_preset_data;
if(!flipper_format_get_value_count(fff_file, "Custom_preset_data", &temp_data32) ||
!temp_data32 || (temp_data32 % 2)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Custom_preset_data size error");
break;
}
custom_preset_data = malloc(temp_data32);
if(!flipper_format_read_hex(
fff_file, "Custom_preset_data", custom_preset_data, temp_data32)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Custom_preset_data read error");
break;
}
subghz_devices_load_preset(js_subghz->radio_device, preset, custom_preset_data);
free(custom_preset_data);
} else {
subghz_devices_load_preset(js_subghz->radio_device, preset, NULL);
}
js_subghz->frequency = subghz_devices_set_frequency(js_subghz->radio_device, frequency);
if(!flipper_format_read_string(fff_file, "Protocol", temp_str)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Missing protocol");
break;
}
bool is_raw = furi_string_equal(temp_str, "RAW");
if(is_raw) {
fff_raw = flipper_format_string_alloc();
subghz_protocol_raw_gen_fff_data(
fff_raw, file_path, subghz_devices_get_name(js_subghz->radio_device));
// One repeat by default
if(!repeat) {
repeat = 1;
}
} else {
// Simulate holding button by default
if(!repeat) {
if(furi_string_equal(temp_str, "CAME Atomo") ||
furi_string_equal(temp_str, "CAME TWEE") ||
furi_string_equal(temp_str, "Hormann HSM") ||
furi_string_equal(temp_str, "Nice FloR-S") ||
furi_string_equal(temp_str, "Power Smart")) {
// These protocols send multiple frames/packets for each "repeat"
// Just 1 full repeat should be sufficient
repeat = 1;
} else {
repeat = 10;
}
}
// Pass repeat value to protocol layer
flipper_format_insert_or_update_uint32(fff_file, "Repeat", &repeat, 1);
// Repeat variable applies to high-level code here, should only loop once
repeat = 1;
}
transmitter = subghz_transmitter_alloc_init(environment, furi_string_get_cstr(temp_str));
if(!transmitter) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to init transmitter");
break;
}
SubGhzProtocolStatus status =
subghz_transmitter_deserialize(transmitter, is_raw ? fff_raw : fff_file);
if(status != SubGhzProtocolStatusOk) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to deserialize protocol");
break;
}
// Must close file here, otherwise RAW protocol cannot open
flipper_format_file_close(fff_file);
if(!js_subghz->is_external) {
furi_hal_power_suppress_charge_enter();
}
subghz_devices_set_tx(js_subghz->radio_device);
FURI_LOG_I(TAG, "Transmitting file %s", file_path);
while(repeat) {
if(!subghz_devices_start_async_tx(
js_subghz->radio_device, subghz_transmitter_yield, transmitter)) {
is_sent = false;
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Failed to start async tx");
break;
}
while(!subghz_devices_is_async_complete_tx(js_subghz->radio_device)) {
furi_delay_ms(100);
}
subghz_devices_stop_async_tx(js_subghz->radio_device);
subghz_transmitter_stop(transmitter);
is_sent = true;
repeat--;
// Only RAW is repeated with this loop, check comments above
if(!is_raw) {
break;
}
if(repeat) {
subghz_transmitter_deserialize(transmitter, fff_raw);
furi_delay_ms(200);
}
}
if(!js_subghz->is_external) {
furi_hal_power_suppress_charge_exit();
}
} while(false);
subghz_devices_idle(js_subghz->radio_device);
js_subghz->state = JsSubghzRadioStateIDLE;
if(transmitter) {
subghz_transmitter_free(transmitter);
}
furi_string_free(temp_str);
subghz_environment_free(environment);
if(fff_raw) {
flipper_format_free(fff_raw);
}
flipper_format_free(fff_file);
furi_record_close(RECORD_STORAGE);
if(is_sent) {
// Return true for backwards compatibility
// Now it will just error if something goes wrong, not return false
mjs_return(mjs, mjs_mk_boolean(mjs, true));
} else {
// Broke out of do...while with an mJS error
mjs_return(mjs, MJS_UNDEFINED);
}
}
static void js_subghz_setup(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is already setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_subghz->radio_device =
radio_device_loader_set(js_subghz->radio_device, SubGhzRadioDeviceTypeExternalCC1101);
if(!subghz_devices_is_connect(js_subghz->radio_device)) {
js_subghz->is_external = true;
} else {
js_subghz->is_external = false;
}
js_subghz->state = JsSubghzRadioStateIDLE;
js_subghz->frequency = 433920000;
subghz_devices_reset(js_subghz->radio_device);
subghz_devices_idle(js_subghz->radio_device);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_subghz_end(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSubghzInst* js_subghz = mjs_get_ptr(mjs, obj_inst);
furi_assert(js_subghz);
if(!js_subghz->radio_device) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Radio is not setup");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
subghz_devices_sleep(js_subghz->radio_device);
radio_device_loader_end(js_subghz->radio_device);
js_subghz->radio_device = NULL;
js_subghz->is_external = false;
js_subghz->state = -1;
js_subghz->frequency = 0;
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_subghz_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsSubghzInst* js_subghz = malloc(sizeof(JsSubghzInst));
mjs_val_t subghz_obj = mjs_mk_object(mjs);
subghz_devices_init();
mjs_set(mjs, subghz_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_subghz));
mjs_set(mjs, subghz_obj, "setup", ~0, MJS_MK_FN(js_subghz_setup));
mjs_set(mjs, subghz_obj, "end", ~0, MJS_MK_FN(js_subghz_end));
mjs_set(mjs, subghz_obj, "setRx", ~0, MJS_MK_FN(js_subghz_set_rx));
mjs_set(mjs, subghz_obj, "setIdle", ~0, MJS_MK_FN(js_subghz_set_idle));
mjs_set(mjs, subghz_obj, "getRssi", ~0, MJS_MK_FN(js_subghz_get_rssi));
mjs_set(mjs, subghz_obj, "getState", ~0, MJS_MK_FN(js_subghz_get_state));
mjs_set(mjs, subghz_obj, "getFrequency", ~0, MJS_MK_FN(js_subghz_get_frequency));
mjs_set(mjs, subghz_obj, "setFrequency", ~0, MJS_MK_FN(js_subghz_set_frequency));
mjs_set(mjs, subghz_obj, "isExternal", ~0, MJS_MK_FN(js_subghz_is_external));
mjs_set(mjs, subghz_obj, "transmitFile", ~0, MJS_MK_FN(js_subghz_transmit_file));
*object = subghz_obj;
return js_subghz;
}
static void js_subghz_destroy(void* inst) {
JsSubghzInst* js_subghz = inst;
if(js_subghz->radio_device) {
subghz_devices_sleep(js_subghz->radio_device);
radio_device_loader_end(js_subghz->radio_device);
}
subghz_devices_deinit();
free(js_subghz);
}
static const JsModuleDescriptor js_subghz_desc = {
"subghz",
js_subghz_create,
js_subghz_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_subghz_desc,
};
const FlipperAppPluginDescriptor* js_subghz_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,64 +0,0 @@
#include "radio_device_loader.h"
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
static void radio_device_loader_power_on() {
uint8_t attempts = 0;
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
furi_hal_power_enable_otg();
//CC1101 power-up time
furi_delay_ms(10);
}
}
static void radio_device_loader_power_off() {
if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
}
bool radio_device_loader_is_connect_external(const char* name) {
bool is_connect = false;
bool is_otg_enabled = furi_hal_power_is_otg_enabled();
if(!is_otg_enabled) {
radio_device_loader_power_on();
}
const SubGhzDevice* device = subghz_devices_get_by_name(name);
if(device) {
is_connect = subghz_devices_is_connect(device);
}
if(!is_otg_enabled) {
radio_device_loader_power_off();
}
return is_connect;
}
const SubGhzDevice* radio_device_loader_set(
const SubGhzDevice* current_radio_device,
SubGhzRadioDeviceType radio_device_type) {
const SubGhzDevice* radio_device;
if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
radio_device_loader_power_on();
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
subghz_devices_begin(radio_device);
} else if(current_radio_device == NULL) {
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
} else {
radio_device_loader_end(current_radio_device);
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
}
return radio_device;
}
void radio_device_loader_end(const SubGhzDevice* radio_device) {
furi_assert(radio_device);
radio_device_loader_power_off();
if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
subghz_devices_end(radio_device);
}
}

View File

@@ -1,15 +0,0 @@
#pragma once
#include <lib/subghz/devices/devices.h>
/** SubGhzRadioDeviceType */
typedef enum {
SubGhzRadioDeviceTypeInternal,
SubGhzRadioDeviceTypeExternalCC1101,
} SubGhzRadioDeviceType;
const SubGhzDevice* radio_device_loader_set(
const SubGhzDevice* current_radio_device,
SubGhzRadioDeviceType radio_device_type);
void radio_device_loader_end(const SubGhzDevice* radio_device);

View File

@@ -1,104 +0,0 @@
#include "../js_modules.h" // IWYU pragma: keep
#include <core/common_defines.h>
#include <furi_hal_version.h>
#include <power/power_service/power.h>
#define TAG "JsTests"
static void js_tests_fail(struct mjs* mjs) {
furi_check(mjs_nargs(mjs) == 1);
mjs_val_t message_arg = mjs_arg(mjs, 0);
const char* message = mjs_get_string(mjs, &message_arg, NULL);
furi_check(message);
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", message);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_tests_assert_eq(struct mjs* mjs) {
furi_check(mjs_nargs(mjs) == 2);
mjs_val_t expected_arg = mjs_arg(mjs, 0);
mjs_val_t result_arg = mjs_arg(mjs, 1);
if(mjs_is_number(expected_arg) && mjs_is_number(result_arg)) {
int32_t expected = mjs_get_int32(mjs, expected_arg);
int32_t result = mjs_get_int32(mjs, result_arg);
if(expected == result) {
FURI_LOG_T(TAG, "eq passed (exp=%ld res=%ld)", expected, result);
} else {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "expected %d, found %d", expected, result);
}
} else if(mjs_is_string(expected_arg) && mjs_is_string(result_arg)) {
const char* expected = mjs_get_string(mjs, &expected_arg, NULL);
const char* result = mjs_get_string(mjs, &result_arg, NULL);
if(strcmp(expected, result) == 0) {
FURI_LOG_T(TAG, "eq passed (exp=\"%s\" res=\"%s\")", expected, result);
} else {
mjs_prepend_errorf(
mjs, MJS_INTERNAL_ERROR, "expected \"%s\", found \"%s\"", expected, result);
}
} else if(mjs_is_boolean(expected_arg) && mjs_is_boolean(result_arg)) {
bool expected = mjs_get_bool(mjs, expected_arg);
bool result = mjs_get_bool(mjs, result_arg);
if(expected == result) {
FURI_LOG_T(
TAG,
"eq passed (exp=%s res=%s)",
expected ? "true" : "false",
result ? "true" : "false");
} else {
mjs_prepend_errorf(
mjs,
MJS_INTERNAL_ERROR,
"expected %s, found %s",
expected ? "true" : "false",
result ? "true" : "false");
}
} else {
JS_ERROR_AND_RETURN(
mjs,
MJS_INTERNAL_ERROR,
"type mismatch (expected %s, result %s)",
mjs_typeof(expected_arg),
mjs_typeof(result_arg));
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_tests_assert_float_close(struct mjs* mjs) {
furi_check(mjs_nargs(mjs) == 3);
mjs_val_t expected_arg = mjs_arg(mjs, 0);
mjs_val_t result_arg = mjs_arg(mjs, 1);
mjs_val_t epsilon_arg = mjs_arg(mjs, 2);
furi_check(mjs_is_number(expected_arg));
furi_check(mjs_is_number(result_arg));
furi_check(mjs_is_number(epsilon_arg));
double expected = mjs_get_double(mjs, expected_arg);
double result = mjs_get_double(mjs, result_arg);
double epsilon = mjs_get_double(mjs, epsilon_arg);
if(ABS(expected - result) > epsilon) {
mjs_prepend_errorf(
mjs,
MJS_INTERNAL_ERROR,
"expected %f found %f (tolerance=%f)",
expected,
result,
epsilon);
}
mjs_return(mjs, MJS_UNDEFINED);
}
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t tests_obj = mjs_mk_object(mjs);
mjs_set(mjs, tests_obj, "fail", ~0, MJS_MK_FN(js_tests_fail));
mjs_set(mjs, tests_obj, "assert_eq", ~0, MJS_MK_FN(js_tests_assert_eq));
mjs_set(mjs, tests_obj, "assert_float_close", ~0, MJS_MK_FN(js_tests_assert_float_close));
*object = tests_obj;
return (void*)1;
}

View File

@@ -1,5 +0,0 @@
#pragma once
#include "../js_thread_i.h"
#include "../js_modules.h"
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);

View File

@@ -1,203 +0,0 @@
#include "../../js_modules.h"
#include <furi_hal_usb.h>
#include <toolbox/path.h>
#include "mass_storage_usb.h"
#define TAG "JsUsbdisk"
typedef struct {
File* file;
char* path;
MassStorageUsb* usb;
bool was_ejected;
} JsUsbdiskInst;
static bool file_read(
void* ctx,
uint32_t lba,
uint16_t count,
uint8_t* out,
uint32_t* out_len,
uint32_t out_cap) {
JsUsbdiskInst* usbdisk = ctx;
FURI_LOG_T(TAG, "file_read lba=%08lX count=%04X out_cap=%08lX", lba, count, out_cap);
if(!storage_file_seek(usbdisk->file, lba * SCSI_BLOCK_SIZE, true)) {
FURI_LOG_W(TAG, "seek failed");
return false;
}
uint16_t clamp = MIN(out_cap, count * SCSI_BLOCK_SIZE);
*out_len = storage_file_read(usbdisk->file, out, clamp);
FURI_LOG_T(TAG, "%lu/%lu", *out_len, count * SCSI_BLOCK_SIZE);
return *out_len == clamp;
}
static bool file_write(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len) {
JsUsbdiskInst* usbdisk = ctx;
FURI_LOG_T(TAG, "file_write lba=%08lX count=%04X len=%08lX", lba, count, len);
if(len != count * SCSI_BLOCK_SIZE) {
FURI_LOG_W(TAG, "bad write params count=%u len=%lu", count, len);
return false;
}
if(!storage_file_seek(usbdisk->file, lba * SCSI_BLOCK_SIZE, true)) {
FURI_LOG_W(TAG, "seek failed");
return false;
}
return storage_file_write(usbdisk->file, buf, len) == len;
}
static uint32_t file_num_blocks(void* ctx) {
JsUsbdiskInst* usbdisk = ctx;
return storage_file_size(usbdisk->file) / SCSI_BLOCK_SIZE;
}
static void file_eject(void* ctx) {
JsUsbdiskInst* usbdisk = ctx;
FURI_LOG_D(TAG, "EJECT");
usbdisk->was_ejected = true;
}
static void js_usbdisk_internal_stop_free(JsUsbdiskInst* usbdisk) {
if(usbdisk->usb) {
mass_storage_usb_stop(usbdisk->usb);
usbdisk->usb = NULL;
}
if(usbdisk->file) {
storage_file_free(usbdisk->file);
furi_record_close(RECORD_STORAGE);
usbdisk->file = NULL;
}
if(usbdisk->path) {
free(usbdisk->path);
usbdisk->path = NULL;
}
}
static void js_usbdisk_start(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUsbdiskInst* usbdisk = mjs_get_ptr(mjs, obj_inst);
furi_assert(usbdisk);
if(usbdisk->usb) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "SCSI is already started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
const char* error = NULL;
do {
if(mjs_nargs(mjs) != 1) {
error = "Wrong argument count";
break;
}
mjs_val_t path_arg = mjs_arg(mjs, 0);
if(!mjs_is_string(path_arg)) {
error = "Path must be a string";
break;
}
size_t path_len = 0;
const char* path = mjs_get_string(mjs, &path_arg, &path_len);
if((path_len == 0) || (path == NULL)) {
error = "Bad path argument";
break;
}
usbdisk->path = strdup(path);
usbdisk->file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
if(!storage_file_open(
usbdisk->file, usbdisk->path, FSAM_READ | FSAM_WRITE, FSOM_OPEN_EXISTING)) {
error = storage_file_get_error_desc(usbdisk->file);
break;
}
} while(0);
if(error) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
js_usbdisk_internal_stop_free(usbdisk);
mjs_return(mjs, MJS_UNDEFINED);
return;
}
SCSIDeviceFunc fn = {
.ctx = usbdisk,
.read = file_read,
.write = file_write,
.num_blocks = file_num_blocks,
.eject = file_eject,
};
furi_hal_usb_unlock();
usbdisk->was_ejected = false;
FuriString* name = furi_string_alloc();
path_extract_filename_no_ext(usbdisk->path, name);
usbdisk->usb = mass_storage_usb_start(furi_string_get_cstr(name), fn);
furi_string_free(name);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_usbdisk_was_ejected(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUsbdiskInst* usbdisk = mjs_get_ptr(mjs, obj_inst);
furi_assert(usbdisk);
if(!usbdisk->usb) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "SCSI is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_return(mjs, mjs_mk_boolean(mjs, usbdisk->was_ejected));
}
static void js_usbdisk_stop(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUsbdiskInst* usbdisk = mjs_get_ptr(mjs, obj_inst);
furi_assert(usbdisk);
if(!usbdisk->usb) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "SCSI is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_usbdisk_internal_stop_free(usbdisk);
mjs_return(mjs, MJS_UNDEFINED);
}
static void* js_usbdisk_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsUsbdiskInst* usbdisk = malloc(sizeof(JsUsbdiskInst));
mjs_val_t usbdisk_obj = mjs_mk_object(mjs);
mjs_set(mjs, usbdisk_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, usbdisk));
mjs_set(mjs, usbdisk_obj, "start", ~0, MJS_MK_FN(js_usbdisk_start));
mjs_set(mjs, usbdisk_obj, "stop", ~0, MJS_MK_FN(js_usbdisk_stop));
mjs_set(mjs, usbdisk_obj, "wasEjected", ~0, MJS_MK_FN(js_usbdisk_was_ejected));
*object = usbdisk_obj;
return usbdisk;
}
static void js_usbdisk_destroy(void* inst) {
JsUsbdiskInst* usbdisk = inst;
js_usbdisk_internal_stop_free(usbdisk);
free(usbdisk);
}
static const JsModuleDescriptor js_usbdisk_desc = {
"usbdisk",
js_usbdisk_create,
js_usbdisk_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_usbdisk_desc,
};
const FlipperAppPluginDescriptor* js_usbdisk_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,266 +0,0 @@
#include "mass_storage_scsi.h"
#include <core/log.h>
#define TAG "MassStorageSCSI"
#define SCSI_TEST_UNIT_READY (0x00)
#define SCSI_REQUEST_SENSE (0x03)
#define SCSI_INQUIRY (0x12)
#define SCSI_READ_FORMAT_CAPACITIES (0x23)
#define SCSI_READ_CAPACITY_10 (0x25)
#define SCSI_MODE_SENSE_6 (0x1A)
#define SCSI_READ_10 (0x28)
#define SCSI_PREVENT_MEDIUM_REMOVAL (0x1E)
#define SCSI_START_STOP_UNIT (0x1B)
#define SCSI_WRITE_10 (0x2A)
bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len) {
if(!len) {
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
return false;
}
FURI_LOG_T(TAG, "START %02X", cmd[0]);
scsi->cmd = cmd;
scsi->cmd_len = len;
scsi->rx_done = false;
scsi->tx_done = false;
switch(cmd[0]) {
case SCSI_WRITE_10: {
if(len < 10) return false;
scsi->write_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
scsi->write_10.count = cmd[7] << 8 | cmd[8];
FURI_LOG_D(TAG, "SCSI_WRITE_10 %08lX %04X", scsi->write_10.lba, scsi->write_10.count);
return true;
}; break;
case SCSI_READ_10: {
if(len < 10) return false;
scsi->read_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
scsi->read_10.count = cmd[7] << 8 | cmd[8];
FURI_LOG_D(TAG, "SCSI_READ_10 %08lX %04X", scsi->read_10.lba, scsi->read_10.count);
return true;
}; break;
}
return true;
}
bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len) {
FURI_LOG_T(TAG, "RX %02X len %lu", scsi->cmd[0], len);
if(scsi->rx_done) return false;
switch(scsi->cmd[0]) {
case SCSI_WRITE_10: {
uint32_t block_size = SCSI_BLOCK_SIZE;
uint16_t blocks = len / block_size;
bool result =
scsi->fn.write(scsi->fn.ctx, scsi->write_10.lba, blocks, data, blocks * block_size);
scsi->write_10.lba += blocks;
scsi->write_10.count -= blocks;
if(!scsi->write_10.count) {
scsi->rx_done = true;
}
return result;
}; break;
default: {
FURI_LOG_W(TAG, "unexpected scsi rx data cmd=%02X", scsi->cmd[0]);
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
return false;
}; break;
}
}
bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap) {
FURI_LOG_T(TAG, "TX %02X cap %lu", scsi->cmd[0], cap);
if(scsi->tx_done) return false;
switch(scsi->cmd[0]) {
case SCSI_REQUEST_SENSE: {
FURI_LOG_D(TAG, "SCSI_REQUEST_SENSE");
if(cap < 18) return false;
memset(data, 0, cap);
data[0] = 0x70; // fixed format sense data
data[1] = 0; // obsolete
data[2] = scsi->sk; // sense key
data[3] = 0; // information
data[4] = 0; // information
data[5] = 0; // information
data[6] = 0; // information
data[7] = 10; // additional sense length (len-8)
data[8] = 0; // command specific information
data[9] = 0; // command specific information
data[10] = 0; // command specific information
data[11] = 0; // command specific information
data[12] = scsi->asc; // additional sense code
data[13] = 0; // additional sense code qualifier
data[14] = 0; // field replaceable unit code
data[15] = 0; // sense key specific information
data[16] = 0; // sense key specific information
data[17] = 0; // sense key specific information
*len = 18;
scsi->sk = 0;
scsi->asc = 0;
scsi->tx_done = true;
return true;
}; break;
case SCSI_INQUIRY: {
FURI_LOG_D(TAG, "SCSI_INQUIRY");
if(scsi->cmd_len < 5) return false;
if(cap < 36) return false;
bool evpd = scsi->cmd[1] & 1;
uint8_t page_code = scsi->cmd[2];
if(evpd == 0) {
if(page_code != 0) return false;
data[0] = 0x00; // device type: direct access block device
data[1] = 0x80; // removable: true
data[2] = 0x04; // version
data[3] = 0x02; // response data format
data[4] = 31; // additional length (len - 5)
data[5] = 0; // flags
data[6] = 0; // flags
data[7] = 0; // flags
memcpy(data + 8, "Flipper ", 8); // vendor id
memcpy(data + 16, "Mass Storage ", 16); // product id
memcpy(data + 32, "0001", 4); // product revision level
*len = 36;
scsi->tx_done = true;
return true;
} else {
if(page_code != 0x80) {
FURI_LOG_W(TAG, "Unsupported VPD code %02X", page_code);
return false;
}
data[0] = 0x00;
data[1] = 0x80;
data[2] = 0x00;
data[3] = 0x01; // Serial len
data[4] = '0';
*len = 5;
scsi->tx_done = true;
return true;
}
}; break;
case SCSI_READ_FORMAT_CAPACITIES: {
FURI_LOG_D(TAG, "SCSI_READ_FORMAT_CAPACITIES");
if(cap < 12) {
return false;
}
uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
uint32_t block_size = SCSI_BLOCK_SIZE;
// Capacity List Header
data[0] = 0;
data[1] = 0;
data[2] = 0;
data[3] = 8;
// Capacity Descriptor
data[4] = (n_blocks - 1) >> 24;
data[5] = (n_blocks - 1) >> 16;
data[6] = (n_blocks - 1) >> 8;
data[7] = (n_blocks - 1) & 0xFF;
data[8] = 0x02; // Formatted media
data[9] = block_size >> 16;
data[10] = block_size >> 8;
data[11] = block_size & 0xFF;
*len = 12;
scsi->tx_done = true;
return true;
}; break;
case SCSI_READ_CAPACITY_10: {
FURI_LOG_D(TAG, "SCSI_READ_CAPACITY_10");
if(cap < 8) return false;
uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
uint32_t block_size = SCSI_BLOCK_SIZE;
data[0] = (n_blocks - 1) >> 24;
data[1] = (n_blocks - 1) >> 16;
data[2] = (n_blocks - 1) >> 8;
data[3] = (n_blocks - 1) & 0xFF;
data[4] = block_size >> 24;
data[5] = block_size >> 16;
data[6] = block_size >> 8;
data[7] = block_size & 0xFF;
*len = 8;
scsi->tx_done = true;
return true;
}; break;
case SCSI_MODE_SENSE_6: {
FURI_LOG_D(TAG, "SCSI_MODE_SENSE_6 %lu", cap);
if(cap < 4) return false;
data[0] = 3; // mode data length (len - 1)
data[1] = 0; // medium type
data[2] = 0; // device-specific parameter
data[3] = 0; // block descriptor length
*len = 4;
scsi->tx_done = true;
return true;
}; break;
case SCSI_READ_10: {
uint32_t block_size = SCSI_BLOCK_SIZE;
bool result =
scsi->fn.read(scsi->fn.ctx, scsi->read_10.lba, scsi->read_10.count, data, len, cap);
*len -= *len % block_size;
uint16_t blocks = *len / block_size;
scsi->read_10.lba += blocks;
scsi->read_10.count -= blocks;
if(!scsi->read_10.count) {
scsi->tx_done = true;
}
return result;
}; break;
default: {
FURI_LOG_W(TAG, "unexpected scsi tx data cmd=%02X", scsi->cmd[0]);
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
return false;
}; break;
}
}
bool scsi_cmd_end(SCSISession* scsi) {
FURI_LOG_T(TAG, "END %02X", scsi->cmd[0]);
uint8_t* cmd = scsi->cmd;
uint8_t len = scsi->cmd_len;
scsi->cmd = NULL;
scsi->cmd_len = 0;
switch(cmd[0]) {
case SCSI_WRITE_10:
return scsi->rx_done;
case SCSI_REQUEST_SENSE:
case SCSI_INQUIRY:
case SCSI_READ_FORMAT_CAPACITIES:
case SCSI_READ_CAPACITY_10:
case SCSI_MODE_SENSE_6:
case SCSI_READ_10:
return scsi->tx_done;
case SCSI_TEST_UNIT_READY: {
FURI_LOG_D(TAG, "SCSI_TEST_UNIT_READY");
return true;
}; break;
case SCSI_PREVENT_MEDIUM_REMOVAL: {
if(len < 6) return false;
bool prevent = cmd[5];
FURI_LOG_D(TAG, "SCSI_PREVENT_MEDIUM_REMOVAL prevent=%d", prevent);
return !prevent;
}; break;
case SCSI_START_STOP_UNIT: {
if(len < 6) return false;
bool eject = (cmd[4] & 2) != 0;
bool start = (cmd[4] & 1) != 0;
FURI_LOG_D(TAG, "SCSI_START_STOP_UNIT eject=%d start=%d", eject, start);
if(eject) {
scsi->fn.eject(scsi->fn.ctx);
}
return true;
}; break;
default: {
FURI_LOG_W(TAG, "unexpected scsi cmd=%02X", cmd[0]);
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
return false;
}; break;
}
}

View File

@@ -1,56 +0,0 @@
#pragma once
#include <furi.h>
#define SCSI_BLOCK_SIZE (0x200UL)
#define SCSI_SK_ILLEGAL_REQUEST (5)
#define SCSI_ASC_INVALID_COMMAND_OPERATION_CODE (0x20)
#define SCSI_ASC_LBA_OOB (0x21)
#define SCSI_ASC_INVALID_FIELD_IN_CDB (0x24)
typedef struct {
void* ctx;
bool (*read)(
void* ctx,
uint32_t lba,
uint16_t count,
uint8_t* out,
uint32_t* out_len,
uint32_t out_cap);
bool (*write)(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len);
uint32_t (*num_blocks)(void* ctx);
void (*eject)(void* ctx);
} SCSIDeviceFunc;
typedef struct {
SCSIDeviceFunc fn;
uint8_t* cmd;
uint8_t cmd_len;
bool rx_done;
bool tx_done;
uint8_t sk; // sense key
uint8_t asc; // additional sense code
// command-specific data
// valid from cmd_start to cmd_end
union {
struct {
uint16_t count;
uint32_t lba;
} read_10; // SCSI_READ_10
struct {
uint16_t count;
uint32_t lba;
} write_10; // SCSI_WRITE_10
};
} SCSISession;
bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len);
bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len);
bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap);
bool scsi_cmd_end(SCSISession* scsi);

View File

@@ -1,484 +0,0 @@
#include "mass_storage_usb.h"
#include <furi_hal.h>
#define TAG "MassStorageUsb"
#define USB_MSC_RX_EP (0x01)
#define USB_MSC_TX_EP (0x82)
#define USB_MSC_RX_EP_SIZE (64UL)
#define USB_MSC_TX_EP_SIZE (64UL)
#define USB_MSC_BOT_GET_MAX_LUN (0xFE)
#define USB_MSC_BOT_RESET (0xFF)
#define CBW_SIG (0x43425355)
#define CBW_FLAGS_DEVICE_TO_HOST (0x80)
#define CSW_SIG (0x53425355)
#define CSW_STATUS_OK (0)
#define CSW_STATUS_NOK (1)
#define CSW_STATUS_PHASE_ERROR (2)
// must be SCSI_BLOCK_SIZE aligned
// larger than 0x10000 exceeds size_t, storage_file_* ops fail
#define USB_MSC_BUF_MAX (0x10000UL - SCSI_BLOCK_SIZE)
static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg);
static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback);
typedef enum {
EventExit = 1 << 0,
EventReset = 1 << 1,
EventRxTx = 1 << 2,
EventAll = EventExit | EventReset | EventRxTx,
} MassStorageEvent;
typedef struct {
uint32_t sig;
uint32_t tag;
uint32_t len;
uint8_t flags;
uint8_t lun;
uint8_t cmd_len;
uint8_t cmd[16];
} __attribute__((packed)) CBW;
typedef struct {
uint32_t sig;
uint32_t tag;
uint32_t residue;
uint8_t status;
} __attribute__((packed)) CSW;
struct MassStorageUsb {
FuriHalUsbInterface usb;
FuriHalUsbInterface* usb_prev;
FuriThread* thread;
usbd_device* dev;
SCSIDeviceFunc fn;
};
static int32_t mass_thread_worker(void* context) {
MassStorageUsb* mass = context;
usbd_device* dev = mass->dev;
SCSISession scsi = {
.fn = mass->fn,
};
CBW cbw = {0};
CSW csw = {0};
uint8_t* buf = NULL;
uint32_t buf_len = 0, buf_cap = 0, buf_sent = 0;
enum {
StateReadCBW,
StateReadData,
StateWriteData,
StateBuildCSW,
StateWriteCSW,
} state = StateReadCBW;
while(true) {
uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever);
if(flags & EventExit) {
FURI_LOG_D(TAG, "exit");
break;
}
if(flags & EventReset) {
FURI_LOG_D(TAG, "reset");
scsi.sk = 0;
scsi.asc = 0;
memset(&cbw, 0, sizeof(cbw));
memset(&csw, 0, sizeof(csw));
if(buf) {
free(buf);
buf = NULL;
}
buf_len = buf_cap = buf_sent = 0;
state = StateReadCBW;
mass->fn.eject(mass->fn.ctx);
}
if(flags & EventRxTx) do {
switch(state) {
case StateReadCBW: {
FURI_LOG_T(TAG, "StateReadCBW");
int32_t len = usbd_ep_read(dev, USB_MSC_RX_EP, &cbw, sizeof(cbw));
if(len <= 0) {
FURI_LOG_T(TAG, "cbw not ready");
break;
}
if(len != sizeof(cbw) || cbw.sig != CBW_SIG) {
FURI_LOG_W(TAG, "bad cbw sig=%08lx", cbw.sig);
usbd_ep_stall(dev, USB_MSC_TX_EP);
usbd_ep_stall(dev, USB_MSC_RX_EP);
continue;
}
if(!scsi_cmd_start(&scsi, cbw.cmd, cbw.cmd_len)) {
FURI_LOG_W(TAG, "bad cmd");
usbd_ep_stall(dev, USB_MSC_RX_EP);
csw.sig = CSW_SIG;
csw.tag = cbw.tag;
csw.status = CSW_STATUS_NOK;
state = StateWriteCSW;
continue;
}
if(cbw.flags & CBW_FLAGS_DEVICE_TO_HOST) {
buf_len = 0;
buf_sent = 0;
state = StateWriteData;
} else {
buf_len = 0;
state = StateReadData;
}
continue;
}; break;
case StateReadData: {
FURI_LOG_T(TAG, "StateReadData %lu/%lu", buf_len, cbw.len);
if(!cbw.len) {
state = StateBuildCSW;
continue;
}
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
if(buf_clamp > buf_cap) {
FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
if(buf) {
free(buf);
}
buf_cap = buf_clamp;
buf = malloc(buf_cap);
}
if(buf_len < buf_clamp) {
int32_t len =
usbd_ep_read(dev, USB_MSC_RX_EP, buf + buf_len, buf_clamp - buf_len);
if(len < 0) {
FURI_LOG_T(TAG, "rx not ready %ld", len);
break;
}
FURI_LOG_T(TAG, "clamp %lu len %ld", buf_clamp, len);
buf_len += len;
}
if(buf_len == buf_clamp) {
if(!scsi_cmd_rx_data(&scsi, buf, buf_len)) {
FURI_LOG_W(TAG, "short rx");
usbd_ep_stall(dev, USB_MSC_RX_EP);
csw.sig = CSW_SIG;
csw.tag = cbw.tag;
csw.status = CSW_STATUS_NOK;
csw.residue = cbw.len;
state = StateWriteCSW;
continue;
}
cbw.len -= buf_len;
buf_len = 0;
}
continue;
}; break;
case StateWriteData: {
FURI_LOG_T(TAG, "StateWriteData %lu", cbw.len);
if(!cbw.len) {
state = StateBuildCSW;
continue;
}
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
if(buf_clamp > buf_cap) {
FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
if(buf) {
free(buf);
}
buf_cap = buf_clamp;
buf = malloc(buf_cap);
}
if(!buf_len && !scsi_cmd_tx_data(&scsi, buf, &buf_len, buf_clamp)) {
FURI_LOG_W(TAG, "short tx");
// usbd_ep_stall(dev, USB_MSC_TX_EP);
state = StateBuildCSW;
continue;
}
int32_t len = usbd_ep_write(
dev,
USB_MSC_TX_EP,
buf + buf_sent,
MIN(USB_MSC_TX_EP_SIZE, buf_len - buf_sent));
if(len < 0) {
FURI_LOG_T(TAG, "tx not ready %ld", len);
break;
}
buf_sent += len;
if(buf_sent == buf_len) {
cbw.len -= buf_len;
buf_len = 0;
buf_sent = 0;
}
continue;
}; break;
case StateBuildCSW: {
FURI_LOG_T(TAG, "StateBuildCSW");
csw.sig = CSW_SIG;
csw.tag = cbw.tag;
if(scsi_cmd_end(&scsi)) {
csw.status = CSW_STATUS_OK;
} else {
csw.status = CSW_STATUS_NOK;
}
csw.residue = cbw.len;
state = StateWriteCSW;
continue;
}; break;
case StateWriteCSW: {
FURI_LOG_T(TAG, "StateWriteCSW");
if(csw.status) {
FURI_LOG_W(
TAG,
"csw sig=%08lx tag=%08lx residue=%08lx status=%02x",
csw.sig,
csw.tag,
csw.residue,
csw.status);
}
int32_t len = usbd_ep_write(dev, USB_MSC_TX_EP, &csw, sizeof(csw));
if(len < 0) {
FURI_LOG_T(TAG, "csw not ready");
break;
}
if(len != sizeof(csw)) {
FURI_LOG_W(TAG, "bad csw write %ld", len);
usbd_ep_stall(dev, USB_MSC_TX_EP);
break;
}
memset(&cbw, 0, sizeof(cbw));
memset(&csw, 0, sizeof(csw));
state = StateReadCBW;
continue;
}; break;
}
break;
} while(true);
}
if(buf) {
free(buf);
}
return 0;
}
// needed in usb_deinit, usb_suspend, usb_rxtx_ep_callback, usb_control,
// where if_ctx isn't passed
static MassStorageUsb* mass_cur = NULL;
static void usb_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) {
UNUSED(intf);
MassStorageUsb* mass = ctx;
mass_cur = mass;
mass->dev = dev;
usbd_reg_config(dev, usb_ep_config);
usbd_reg_control(dev, usb_control);
usbd_connect(dev, true);
mass->thread = furi_thread_alloc();
furi_thread_set_name(mass->thread, "MassStorageUsb");
furi_thread_set_stack_size(mass->thread, 1024);
furi_thread_set_context(mass->thread, ctx);
furi_thread_set_callback(mass->thread, mass_thread_worker);
furi_thread_start(mass->thread);
}
static void usb_deinit(usbd_device* dev) {
usbd_reg_config(dev, NULL);
usbd_reg_control(dev, NULL);
MassStorageUsb* mass = mass_cur;
if(!mass || mass->dev != dev) {
FURI_LOG_E(TAG, "deinit mass_cur leak");
return;
}
mass_cur = NULL;
furi_assert(mass->thread);
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventExit);
furi_thread_join(mass->thread);
furi_thread_free(mass->thread);
mass->thread = NULL;
free(mass->usb.str_prod_descr);
mass->usb.str_prod_descr = NULL;
free(mass->usb.str_serial_descr);
mass->usb.str_serial_descr = NULL;
free(mass);
}
static void usb_wakeup(usbd_device* dev) {
UNUSED(dev);
}
static void usb_suspend(usbd_device* dev) {
MassStorageUsb* mass = mass_cur;
if(!mass || mass->dev != dev) return;
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset);
}
static void usb_rxtx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) {
UNUSED(ep);
UNUSED(event);
MassStorageUsb* mass = mass_cur;
if(!mass || mass->dev != dev) return;
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventRxTx);
}
static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg) {
switch(cfg) {
case 0: // deconfig
usbd_ep_deconfig(dev, USB_MSC_RX_EP);
usbd_ep_deconfig(dev, USB_MSC_TX_EP);
usbd_reg_endpoint(dev, USB_MSC_RX_EP, NULL);
usbd_reg_endpoint(dev, USB_MSC_TX_EP, NULL);
return usbd_ack;
case 1: // config
usbd_ep_config(
dev, USB_MSC_RX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_RX_EP_SIZE);
usbd_ep_config(
dev, USB_MSC_TX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_TX_EP_SIZE);
usbd_reg_endpoint(dev, USB_MSC_RX_EP, usb_rxtx_ep_callback);
usbd_reg_endpoint(dev, USB_MSC_TX_EP, usb_rxtx_ep_callback);
return usbd_ack;
}
return usbd_fail;
}
static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) {
UNUSED(callback);
if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) !=
(USB_REQ_INTERFACE | USB_REQ_CLASS)) {
return usbd_fail;
}
switch(req->bRequest) {
case USB_MSC_BOT_GET_MAX_LUN: {
static uint8_t max_lun = 0;
dev->status.data_ptr = &max_lun;
dev->status.data_count = 1;
return usbd_ack;
}; break;
case USB_MSC_BOT_RESET: {
MassStorageUsb* mass = mass_cur;
if(!mass || mass->dev != dev) return usbd_fail;
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset);
return usbd_ack;
}; break;
}
return usbd_fail;
}
static const struct usb_string_descriptor dev_manuf_desc = USB_STRING_DESC("Flipper Devices Inc.");
struct MassStorageDescriptor {
struct usb_config_descriptor config;
struct usb_interface_descriptor intf;
struct usb_endpoint_descriptor ep_rx;
struct usb_endpoint_descriptor ep_tx;
} __attribute__((packed));
static const struct usb_device_descriptor usb_mass_dev_descr = {
.bLength = sizeof(struct usb_device_descriptor),
.bDescriptorType = USB_DTYPE_DEVICE,
.bcdUSB = VERSION_BCD(2, 0, 0),
.bDeviceClass = USB_CLASS_PER_INTERFACE,
.bDeviceSubClass = USB_SUBCLASS_NONE,
.bDeviceProtocol = USB_PROTO_NONE,
.bMaxPacketSize0 = 8, // USB_EP0_SIZE
.idVendor = 0x0483,
.idProduct = 0x5720,
.bcdDevice = VERSION_BCD(1, 0, 0),
.iManufacturer = 1, // UsbDevManuf
.iProduct = 2, // UsbDevProduct
.iSerialNumber = 3, // UsbDevSerial
.bNumConfigurations = 1,
};
static const struct MassStorageDescriptor usb_mass_cfg_descr = {
.config =
{
.bLength = sizeof(struct usb_config_descriptor),
.bDescriptorType = USB_DTYPE_CONFIGURATION,
.wTotalLength = sizeof(struct MassStorageDescriptor),
.bNumInterfaces = 1,
.bConfigurationValue = 1,
.iConfiguration = NO_DESCRIPTOR,
.bmAttributes = USB_CFG_ATTR_RESERVED | USB_CFG_ATTR_SELFPOWERED,
.bMaxPower = USB_CFG_POWER_MA(100),
},
.intf =
{
.bLength = sizeof(struct usb_interface_descriptor),
.bDescriptorType = USB_DTYPE_INTERFACE,
.bInterfaceNumber = 0,
.bAlternateSetting = 0,
.bNumEndpoints = 2,
.bInterfaceClass = USB_CLASS_MASS_STORAGE,
.bInterfaceSubClass = 0x06, // scsi transparent
.bInterfaceProtocol = 0x50, // bulk only
.iInterface = NO_DESCRIPTOR,
},
.ep_rx =
{
.bLength = sizeof(struct usb_endpoint_descriptor),
.bDescriptorType = USB_DTYPE_ENDPOINT,
.bEndpointAddress = USB_MSC_RX_EP,
.bmAttributes = USB_EPTYPE_BULK,
.wMaxPacketSize = USB_MSC_RX_EP_SIZE,
.bInterval = 0,
},
.ep_tx =
{
.bLength = sizeof(struct usb_endpoint_descriptor),
.bDescriptorType = USB_DTYPE_ENDPOINT,
.bEndpointAddress = USB_MSC_TX_EP,
.bmAttributes = USB_EPTYPE_BULK,
.wMaxPacketSize = USB_MSC_TX_EP_SIZE,
.bInterval = 0,
},
};
MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn) {
MassStorageUsb* mass = malloc(sizeof(MassStorageUsb));
mass->usb_prev = furi_hal_usb_get_config();
mass->usb.init = usb_init;
mass->usb.deinit = usb_deinit;
mass->usb.wakeup = usb_wakeup;
mass->usb.suspend = usb_suspend;
mass->usb.dev_descr = (struct usb_device_descriptor*)&usb_mass_dev_descr;
mass->usb.str_manuf_descr = (void*)&dev_manuf_desc;
mass->usb.str_prod_descr = NULL;
mass->usb.str_serial_descr = NULL;
mass->usb.cfg_descr = (void*)&usb_mass_cfg_descr;
const char* name = furi_hal_version_get_device_name_ptr();
if(!name) name = "Flipper Zero";
size_t len = strlen(name);
struct usb_string_descriptor* str_prod_descr = malloc(len * 2 + 2);
str_prod_descr->bLength = len * 2 + 2;
str_prod_descr->bDescriptorType = USB_DTYPE_STRING;
for(uint8_t i = 0; i < len; i++)
str_prod_descr->wString[i] = name[i];
mass->usb.str_prod_descr = str_prod_descr;
len = strlen(filename);
struct usb_string_descriptor* str_serial_descr = malloc(len * 2 + 2);
str_serial_descr->bLength = len * 2 + 2;
str_serial_descr->bDescriptorType = USB_DTYPE_STRING;
for(uint8_t i = 0; i < len; i++)
str_serial_descr->wString[i] = filename[i];
mass->usb.str_serial_descr = str_serial_descr;
mass->fn = fn;
if(!furi_hal_usb_set_config(&mass->usb, mass)) {
FURI_LOG_E(TAG, "USB locked, cannot start Mass Storage");
free(mass->usb.str_prod_descr);
free(mass->usb.str_serial_descr);
free(mass);
return NULL;
}
return mass;
}
void mass_storage_usb_stop(MassStorageUsb* mass) {
furi_hal_usb_set_config(mass->usb_prev, NULL);
}

View File

@@ -1,9 +0,0 @@
#pragma once
#include <storage/storage.h>
#include "mass_storage_scsi.h"
typedef struct MassStorageUsb MassStorageUsb;
MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn);
void mass_storage_usb_stop(MassStorageUsb* mass);

View File

@@ -1,297 +0,0 @@
#include "ICM42688P_regs.h"
#include "ICM42688P.h"
#define TAG "ICM42688P"
#define ICM42688P_TIMEOUT 100
struct ICM42688P {
FuriHalSpiBusHandle* spi_bus;
const GpioPin* irq_pin;
float accel_scale;
float gyro_scale;
};
static const struct AccelFullScale {
float value;
uint8_t reg_mask;
} accel_fs_modes[] = {
[AccelFullScale16G] = {16.f, ICM42688_AFS_16G},
[AccelFullScale8G] = {8.f, ICM42688_AFS_8G},
[AccelFullScale4G] = {4.f, ICM42688_AFS_4G},
[AccelFullScale2G] = {2.f, ICM42688_AFS_2G},
};
static const struct GyroFullScale {
float value;
uint8_t reg_mask;
} gyro_fs_modes[] = {
[GyroFullScale2000DPS] = {2000.f, ICM42688_GFS_2000DPS},
[GyroFullScale1000DPS] = {1000.f, ICM42688_GFS_1000DPS},
[GyroFullScale500DPS] = {500.f, ICM42688_GFS_500DPS},
[GyroFullScale250DPS] = {250.f, ICM42688_GFS_250DPS},
[GyroFullScale125DPS] = {125.f, ICM42688_GFS_125DPS},
[GyroFullScale62_5DPS] = {62.5f, ICM42688_GFS_62_5DPS},
[GyroFullScale31_25DPS] = {31.25f, ICM42688_GFS_31_25DPS},
[GyroFullScale15_625DPS] = {15.625f, ICM42688_GFS_15_625DPS},
};
static bool icm42688p_write_reg(FuriHalSpiBusHandle* spi_bus, uint8_t addr, uint8_t value) {
bool res = false;
furi_hal_spi_acquire(spi_bus);
do {
uint8_t cmd_data[2] = {addr & 0x7F, value};
if(!furi_hal_spi_bus_tx(spi_bus, cmd_data, 2, ICM42688P_TIMEOUT)) break;
res = true;
} while(0);
furi_hal_spi_release(spi_bus);
return res;
}
static bool icm42688p_read_reg(FuriHalSpiBusHandle* spi_bus, uint8_t addr, uint8_t* value) {
bool res = false;
furi_hal_spi_acquire(spi_bus);
do {
uint8_t cmd_byte = addr | (1 << 7);
if(!furi_hal_spi_bus_tx(spi_bus, &cmd_byte, 1, ICM42688P_TIMEOUT)) break;
if(!furi_hal_spi_bus_rx(spi_bus, value, 1, ICM42688P_TIMEOUT)) break;
res = true;
} while(0);
furi_hal_spi_release(spi_bus);
return res;
}
static bool
icm42688p_read_mem(FuriHalSpiBusHandle* spi_bus, uint8_t addr, uint8_t* data, uint8_t len) {
bool res = false;
furi_hal_spi_acquire(spi_bus);
do {
uint8_t cmd_byte = addr | (1 << 7);
if(!furi_hal_spi_bus_tx(spi_bus, &cmd_byte, 1, ICM42688P_TIMEOUT)) break;
if(!furi_hal_spi_bus_rx(spi_bus, data, len, ICM42688P_TIMEOUT)) break;
res = true;
} while(0);
furi_hal_spi_release(spi_bus);
return res;
}
bool icm42688p_accel_config(
ICM42688P* icm42688p,
ICM42688PAccelFullScale full_scale,
ICM42688PDataRate rate) {
icm42688p->accel_scale = accel_fs_modes[full_scale].value;
uint8_t reg_value = accel_fs_modes[full_scale].reg_mask | rate;
return icm42688p_write_reg(icm42688p->spi_bus, ICM42688_ACCEL_CONFIG0, reg_value);
}
float icm42688p_accel_get_full_scale(ICM42688P* icm42688p) {
return icm42688p->accel_scale;
}
bool icm42688p_gyro_config(
ICM42688P* icm42688p,
ICM42688PGyroFullScale full_scale,
ICM42688PDataRate rate) {
icm42688p->gyro_scale = gyro_fs_modes[full_scale].value;
uint8_t reg_value = gyro_fs_modes[full_scale].reg_mask | rate;
return icm42688p_write_reg(icm42688p->spi_bus, ICM42688_GYRO_CONFIG0, reg_value);
}
float icm42688p_gyro_get_full_scale(ICM42688P* icm42688p) {
return icm42688p->gyro_scale;
}
bool icm42688p_read_accel_raw(ICM42688P* icm42688p, ICM42688PRawData* data) {
bool ret = icm42688p_read_mem(
icm42688p->spi_bus, ICM42688_ACCEL_DATA_X1, (uint8_t*)data, sizeof(ICM42688PRawData));
return ret;
}
bool icm42688p_read_gyro_raw(ICM42688P* icm42688p, ICM42688PRawData* data) {
bool ret = icm42688p_read_mem(
icm42688p->spi_bus, ICM42688_GYRO_DATA_X1, (uint8_t*)data, sizeof(ICM42688PRawData));
return ret;
}
bool icm42688p_write_gyro_offset(ICM42688P* icm42688p, ICM42688PScaledData* scaled_data) {
if((fabsf(scaled_data->x) > 64.f) || (fabsf(scaled_data->y) > 64.f) ||
(fabsf(scaled_data->z) > 64.f)) {
return false;
}
uint16_t offset_x = (uint16_t)(-(int16_t)(scaled_data->x * 32.f) * 16) >> 4;
uint16_t offset_y = (uint16_t)(-(int16_t)(scaled_data->y * 32.f) * 16) >> 4;
uint16_t offset_z = (uint16_t)(-(int16_t)(scaled_data->z * 32.f) * 16) >> 4;
uint8_t offset_regs[9];
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 4);
icm42688p_read_mem(icm42688p->spi_bus, ICM42688_OFFSET_USER0, offset_regs, 5);
offset_regs[0] = offset_x & 0xFF;
offset_regs[1] = (offset_x & 0xF00) >> 8;
offset_regs[1] |= (offset_y & 0xF00) >> 4;
offset_regs[2] = offset_y & 0xFF;
offset_regs[3] = offset_z & 0xFF;
offset_regs[4] &= 0xF0;
offset_regs[4] |= (offset_z & 0x0F00) >> 8;
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER0, offset_regs[0]);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER1, offset_regs[1]);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER2, offset_regs[2]);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER3, offset_regs[3]);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_OFFSET_USER4, offset_regs[4]);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0);
return true;
}
void icm42688p_apply_scale(ICM42688PRawData* raw_data, float full_scale, ICM42688PScaledData* data) {
data->x = ((float)(raw_data->x)) / 32768.f * full_scale;
data->y = ((float)(raw_data->y)) / 32768.f * full_scale;
data->z = ((float)(raw_data->z)) / 32768.f * full_scale;
}
void icm42688p_apply_scale_fifo(
ICM42688P* icm42688p,
ICM42688PFifoPacket* fifo_data,
ICM42688PScaledData* accel_data,
ICM42688PScaledData* gyro_data) {
float full_scale = icm42688p->accel_scale;
accel_data->x = ((float)(fifo_data->a_x)) / 32768.f * full_scale;
accel_data->y = ((float)(fifo_data->a_y)) / 32768.f * full_scale;
accel_data->z = ((float)(fifo_data->a_z)) / 32768.f * full_scale;
full_scale = icm42688p->gyro_scale;
gyro_data->x = ((float)(fifo_data->g_x)) / 32768.f * full_scale;
gyro_data->y = ((float)(fifo_data->g_y)) / 32768.f * full_scale;
gyro_data->z = ((float)(fifo_data->g_z)) / 32768.f * full_scale;
}
float icm42688p_read_temp(ICM42688P* icm42688p) {
uint8_t reg_val[2];
icm42688p_read_mem(icm42688p->spi_bus, ICM42688_TEMP_DATA1, reg_val, 2);
int16_t temp_int = (reg_val[0] << 8) | reg_val[1];
return ((float)temp_int / 132.48f) + 25.f;
}
void icm42688_fifo_enable(
ICM42688P* icm42688p,
ICM42688PIrqCallback irq_callback,
void* irq_context) {
// FIFO mode: stream
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG, (1 << 6));
// Little-endian data, FIFO count in records
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INTF_CONFIG0, (1 << 7) | (1 << 6));
// FIFO partial read, FIFO packet: gyro + accel TODO: 20bit
icm42688p_write_reg(
icm42688p->spi_bus, ICM42688_FIFO_CONFIG1, (1 << 6) | (1 << 5) | (1 << 1) | (1 << 0));
// FIFO irq watermark
uint16_t fifo_watermark = 1;
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG2, fifo_watermark & 0xFF);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG3, fifo_watermark >> 8);
// IRQ1: push-pull, active high
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_CONFIG, (1 << 1) | (1 << 0));
// Clear IRQ on status read
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_CONFIG0, 0);
// IRQ pulse duration
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_CONFIG1, (1 << 6) | (1 << 5));
uint8_t reg_data = 0;
icm42688p_read_reg(icm42688p->spi_bus, ICM42688_INT_STATUS, &reg_data);
furi_hal_gpio_init(icm42688p->irq_pin, GpioModeInterruptRise, GpioPullDown, GpioSpeedVeryHigh);
furi_hal_gpio_remove_int_callback(icm42688p->irq_pin);
furi_hal_gpio_add_int_callback(icm42688p->irq_pin, irq_callback, irq_context);
// IRQ1 source: FIFO threshold
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE0, (1 << 2));
}
void icm42688_fifo_disable(ICM42688P* icm42688p) {
furi_hal_gpio_remove_int_callback(icm42688p->irq_pin);
furi_hal_gpio_init(icm42688p->irq_pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE0, 0);
// FIFO mode: bypass
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_FIFO_CONFIG, 0);
}
uint16_t icm42688_fifo_get_count(ICM42688P* icm42688p) {
uint16_t reg_val = 0;
icm42688p_read_mem(icm42688p->spi_bus, ICM42688_FIFO_COUNTH, (uint8_t*)&reg_val, 2);
return reg_val;
}
bool icm42688_fifo_read(ICM42688P* icm42688p, ICM42688PFifoPacket* data) {
icm42688p_read_mem(
icm42688p->spi_bus, ICM42688_FIFO_DATA, (uint8_t*)data, sizeof(ICM42688PFifoPacket));
return (data->header) & (1 << 7);
}
ICM42688P* icm42688p_alloc(FuriHalSpiBusHandle* spi_bus, const GpioPin* irq_pin) {
ICM42688P* icm42688p = malloc(sizeof(ICM42688P));
icm42688p->spi_bus = spi_bus;
icm42688p->irq_pin = irq_pin;
return icm42688p;
}
void icm42688p_free(ICM42688P* icm42688p) {
free(icm42688p);
}
bool icm42688p_init(ICM42688P* icm42688p) {
furi_hal_spi_bus_handle_init(icm42688p->spi_bus);
// Software reset
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0); // Set reg bank to 0
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_DEVICE_CONFIG, 0x01); // SPI Mode 0, SW reset
furi_delay_ms(1);
uint8_t reg_value = 0;
bool read_ok = icm42688p_read_reg(icm42688p->spi_bus, ICM42688_WHO_AM_I, &reg_value);
if(!read_ok) {
FURI_LOG_E(TAG, "Chip ID read failed");
return false;
} else if(reg_value != ICM42688_WHOAMI) {
FURI_LOG_E(
TAG, "Sensor returned wrong ID 0x%02X, expected 0x%02X", reg_value, ICM42688_WHOAMI);
return false;
}
// Disable all interrupts
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE0, 0);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE1, 0);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE3, 0);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE4, 0);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 4);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE6, 0);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INT_SOURCE7, 0);
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0);
// Data format: little endian
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_INTF_CONFIG0, 0);
// Enable all sensors
icm42688p_write_reg(
icm42688p->spi_bus,
ICM42688_PWR_MGMT0,
ICM42688_PWR_TEMP_ON | ICM42688_PWR_GYRO_MODE_LN | ICM42688_PWR_ACCEL_MODE_LN);
furi_delay_ms(45);
icm42688p_accel_config(icm42688p, AccelFullScale16G, DataRate1kHz);
icm42688p_gyro_config(icm42688p, GyroFullScale2000DPS, DataRate1kHz);
return true;
}
bool icm42688p_deinit(ICM42688P* icm42688p) {
// Software reset
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_REG_BANK_SEL, 0); // Set reg bank to 0
icm42688p_write_reg(icm42688p->spi_bus, ICM42688_DEVICE_CONFIG, 0x01); // SPI Mode 0, SW reset
furi_hal_spi_bus_handle_deinit(icm42688p->spi_bus);
return true;
}

View File

@@ -1,127 +0,0 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
DataRate32kHz = 0x01,
DataRate16kHz = 0x02,
DataRate8kHz = 0x03,
DataRate4kHz = 0x04,
DataRate2kHz = 0x05,
DataRate1kHz = 0x06,
DataRate200Hz = 0x07,
DataRate100Hz = 0x08,
DataRate50Hz = 0x09,
DataRate25Hz = 0x0A,
DataRate12_5Hz = 0x0B,
DataRate6_25Hz = 0x0C, // Accelerometer only
DataRate3_125Hz = 0x0D, // Accelerometer only
DataRate1_5625Hz = 0x0E, // Accelerometer only
DataRate500Hz = 0x0F,
} ICM42688PDataRate;
typedef enum {
AccelFullScale16G = 0,
AccelFullScale8G,
AccelFullScale4G,
AccelFullScale2G,
AccelFullScaleTotal,
} ICM42688PAccelFullScale;
typedef enum {
GyroFullScale2000DPS = 0,
GyroFullScale1000DPS,
GyroFullScale500DPS,
GyroFullScale250DPS,
GyroFullScale125DPS,
GyroFullScale62_5DPS,
GyroFullScale31_25DPS,
GyroFullScale15_625DPS,
GyroFullScaleTotal,
} ICM42688PGyroFullScale;
typedef struct {
int16_t x;
int16_t y;
int16_t z;
} __attribute__((packed)) ICM42688PRawData;
typedef struct {
uint8_t header;
int16_t a_x;
int16_t a_y;
int16_t a_z;
int16_t g_x;
int16_t g_y;
int16_t g_z;
uint8_t temp;
uint16_t ts;
} __attribute__((packed)) ICM42688PFifoPacket;
typedef struct {
float x;
float y;
float z;
} ICM42688PScaledData;
typedef struct ICM42688P ICM42688P;
typedef void (*ICM42688PIrqCallback)(void* ctx);
ICM42688P* icm42688p_alloc(FuriHalSpiBusHandle* spi_bus, const GpioPin* irq_pin);
bool icm42688p_init(ICM42688P* icm42688p);
bool icm42688p_deinit(ICM42688P* icm42688p);
void icm42688p_free(ICM42688P* icm42688p);
bool icm42688p_accel_config(
ICM42688P* icm42688p,
ICM42688PAccelFullScale full_scale,
ICM42688PDataRate rate);
float icm42688p_accel_get_full_scale(ICM42688P* icm42688p);
bool icm42688p_gyro_config(
ICM42688P* icm42688p,
ICM42688PGyroFullScale full_scale,
ICM42688PDataRate rate);
float icm42688p_gyro_get_full_scale(ICM42688P* icm42688p);
bool icm42688p_read_accel_raw(ICM42688P* icm42688p, ICM42688PRawData* data);
bool icm42688p_read_gyro_raw(ICM42688P* icm42688p, ICM42688PRawData* data);
bool icm42688p_write_gyro_offset(ICM42688P* icm42688p, ICM42688PScaledData* scaled_data);
void icm42688p_apply_scale(ICM42688PRawData* raw_data, float full_scale, ICM42688PScaledData* data);
void icm42688p_apply_scale_fifo(
ICM42688P* icm42688p,
ICM42688PFifoPacket* fifo_data,
ICM42688PScaledData* accel_data,
ICM42688PScaledData* gyro_data);
float icm42688p_read_temp(ICM42688P* icm42688p);
void icm42688_fifo_enable(
ICM42688P* icm42688p,
ICM42688PIrqCallback irq_callback,
void* irq_context);
void icm42688_fifo_disable(ICM42688P* icm42688p);
uint16_t icm42688_fifo_get_count(ICM42688P* icm42688p);
bool icm42688_fifo_read(ICM42688P* icm42688p, ICM42688PFifoPacket* data);
#ifdef __cplusplus
}
#endif

View File

@@ -1,176 +0,0 @@
#pragma once
#define ICM42688_WHOAMI 0x47
// Bank 0
#define ICM42688_DEVICE_CONFIG 0x11
#define ICM42688_DRIVE_CONFIG 0x13
#define ICM42688_INT_CONFIG 0x14
#define ICM42688_FIFO_CONFIG 0x16
#define ICM42688_TEMP_DATA1 0x1D
#define ICM42688_TEMP_DATA0 0x1E
#define ICM42688_ACCEL_DATA_X1 0x1F
#define ICM42688_ACCEL_DATA_X0 0x20
#define ICM42688_ACCEL_DATA_Y1 0x21
#define ICM42688_ACCEL_DATA_Y0 0x22
#define ICM42688_ACCEL_DATA_Z1 0x23
#define ICM42688_ACCEL_DATA_Z0 0x24
#define ICM42688_GYRO_DATA_X1 0x25
#define ICM42688_GYRO_DATA_X0 0x26
#define ICM42688_GYRO_DATA_Y1 0x27
#define ICM42688_GYRO_DATA_Y0 0x28
#define ICM42688_GYRO_DATA_Z1 0x29
#define ICM42688_GYRO_DATA_Z0 0x2A
#define ICM42688_TMST_FSYNCH 0x2B
#define ICM42688_TMST_FSYNCL 0x2C
#define ICM42688_INT_STATUS 0x2D
#define ICM42688_FIFO_COUNTH 0x2E
#define ICM42688_FIFO_COUNTL 0x2F
#define ICM42688_FIFO_DATA 0x30
#define ICM42688_APEX_DATA0 0x31
#define ICM42688_APEX_DATA1 0x32
#define ICM42688_APEX_DATA2 0x33
#define ICM42688_APEX_DATA3 0x34
#define ICM42688_APEX_DATA4 0x35
#define ICM42688_APEX_DATA5 0x36
#define ICM42688_INT_STATUS2 0x37
#define ICM42688_INT_STATUS3 0x38
#define ICM42688_SIGNAL_PATH_RESET 0x4B
#define ICM42688_INTF_CONFIG0 0x4C
#define ICM42688_INTF_CONFIG1 0x4D
#define ICM42688_PWR_MGMT0 0x4E
#define ICM42688_GYRO_CONFIG0 0x4F
#define ICM42688_ACCEL_CONFIG0 0x50
#define ICM42688_GYRO_CONFIG1 0x51
#define ICM42688_GYRO_ACCEL_CONFIG0 0x52
#define ICM42688_ACCEL_CONFIG1 0x53
#define ICM42688_TMST_CONFIG 0x54
#define ICM42688_APEX_CONFIG0 0x56
#define ICM42688_SMD_CONFIG 0x57
#define ICM42688_FIFO_CONFIG1 0x5F
#define ICM42688_FIFO_CONFIG2 0x60
#define ICM42688_FIFO_CONFIG3 0x61
#define ICM42688_FSYNC_CONFIG 0x62
#define ICM42688_INT_CONFIG0 0x63
#define ICM42688_INT_CONFIG1 0x64
#define ICM42688_INT_SOURCE0 0x65
#define ICM42688_INT_SOURCE1 0x66
#define ICM42688_INT_SOURCE3 0x68
#define ICM42688_INT_SOURCE4 0x69
#define ICM42688_FIFO_LOST_PKT0 0x6C
#define ICM42688_FIFO_LOST_PKT1 0x6D
#define ICM42688_SELF_TEST_CONFIG 0x70
#define ICM42688_WHO_AM_I 0x75
#define ICM42688_REG_BANK_SEL 0x76
// Bank 1
#define ICM42688_SENSOR_CONFIG0 0x03
#define ICM42688_GYRO_CONFIG_STATIC2 0x0B
#define ICM42688_GYRO_CONFIG_STATIC3 0x0C
#define ICM42688_GYRO_CONFIG_STATIC4 0x0D
#define ICM42688_GYRO_CONFIG_STATIC5 0x0E
#define ICM42688_GYRO_CONFIG_STATIC6 0x0F
#define ICM42688_GYRO_CONFIG_STATIC7 0x10
#define ICM42688_GYRO_CONFIG_STATIC8 0x11
#define ICM42688_GYRO_CONFIG_STATIC9 0x12
#define ICM42688_GYRO_CONFIG_STATIC10 0x13
#define ICM42688_XG_ST_DATA 0x5F
#define ICM42688_YG_ST_DATA 0x60
#define ICM42688_ZG_ST_DATA 0x61
#define ICM42688_TMSTVAL0 0x62
#define ICM42688_TMSTVAL1 0x63
#define ICM42688_TMSTVAL2 0x64
#define ICM42688_INTF_CONFIG4 0x7A
#define ICM42688_INTF_CONFIG5 0x7B
#define ICM42688_INTF_CONFIG6 0x7C
// Bank 2
#define ICM42688_ACCEL_CONFIG_STATIC2 0x03
#define ICM42688_ACCEL_CONFIG_STATIC3 0x04
#define ICM42688_ACCEL_CONFIG_STATIC4 0x05
#define ICM42688_XA_ST_DATA 0x3B
#define ICM42688_YA_ST_DATA 0x3C
#define ICM42688_ZA_ST_DATA 0x3D
// Bank 4
#define ICM42688_APEX_CONFIG1 0x40
#define ICM42688_APEX_CONFIG2 0x41
#define ICM42688_APEX_CONFIG3 0x42
#define ICM42688_APEX_CONFIG4 0x43
#define ICM42688_APEX_CONFIG5 0x44
#define ICM42688_APEX_CONFIG6 0x45
#define ICM42688_APEX_CONFIG7 0x46
#define ICM42688_APEX_CONFIG8 0x47
#define ICM42688_APEX_CONFIG9 0x48
#define ICM42688_ACCEL_WOM_X_THR 0x4A
#define ICM42688_ACCEL_WOM_Y_THR 0x4B
#define ICM42688_ACCEL_WOM_Z_THR 0x4C
#define ICM42688_INT_SOURCE6 0x4D
#define ICM42688_INT_SOURCE7 0x4E
#define ICM42688_INT_SOURCE8 0x4F
#define ICM42688_INT_SOURCE9 0x50
#define ICM42688_INT_SOURCE10 0x51
#define ICM42688_OFFSET_USER0 0x77
#define ICM42688_OFFSET_USER1 0x78
#define ICM42688_OFFSET_USER2 0x79
#define ICM42688_OFFSET_USER3 0x7A
#define ICM42688_OFFSET_USER4 0x7B
#define ICM42688_OFFSET_USER5 0x7C
#define ICM42688_OFFSET_USER6 0x7D
#define ICM42688_OFFSET_USER7 0x7E
#define ICM42688_OFFSET_USER8 0x7F
// PWR_MGMT0
#define ICM42688_PWR_TEMP_ON (0 << 5)
#define ICM42688_PWR_TEMP_OFF (1 << 5)
#define ICM42688_PWR_IDLE (1 << 4)
#define ICM42688_PWR_GYRO_MODE_OFF (0 << 2)
#define ICM42688_PWR_GYRO_MODE_LN (3 << 2)
#define ICM42688_PWR_ACCEL_MODE_OFF (0 << 0)
#define ICM42688_PWR_ACCEL_MODE_LP (2 << 0)
#define ICM42688_PWR_ACCEL_MODE_LN (3 << 0)
// GYRO_CONFIG0
#define ICM42688_GFS_2000DPS (0x00 << 5)
#define ICM42688_GFS_1000DPS (0x01 << 5)
#define ICM42688_GFS_500DPS (0x02 << 5)
#define ICM42688_GFS_250DPS (0x03 << 5)
#define ICM42688_GFS_125DPS (0x04 << 5)
#define ICM42688_GFS_62_5DPS (0x05 << 5)
#define ICM42688_GFS_31_25DPS (0x06 << 5)
#define ICM42688_GFS_15_625DPS (0x07 << 5)
#define ICM42688_GODR_32kHz 0x01
#define ICM42688_GODR_16kHz 0x02
#define ICM42688_GODR_8kHz 0x03
#define ICM42688_GODR_4kHz 0x04
#define ICM42688_GODR_2kHz 0x05
#define ICM42688_GODR_1kHz 0x06
#define ICM42688_GODR_200Hz 0x07
#define ICM42688_GODR_100Hz 0x08
#define ICM42688_GODR_50Hz 0x09
#define ICM42688_GODR_25Hz 0x0A
#define ICM42688_GODR_12_5Hz 0x0B
#define ICM42688_GODR_500Hz 0x0F
// ACCEL_CONFIG0
#define ICM42688_AFS_16G (0x00 << 5)
#define ICM42688_AFS_8G (0x01 << 5)
#define ICM42688_AFS_4G (0x02 << 5)
#define ICM42688_AFS_2G (0x03 << 5)
#define ICM42688_AODR_32kHz 0x01
#define ICM42688_AODR_16kHz 0x02
#define ICM42688_AODR_8kHz 0x03
#define ICM42688_AODR_4kHz 0x04
#define ICM42688_AODR_2kHz 0x05
#define ICM42688_AODR_1kHz 0x06
#define ICM42688_AODR_200Hz 0x07
#define ICM42688_AODR_100Hz 0x08
#define ICM42688_AODR_50Hz 0x09
#define ICM42688_AODR_25Hz 0x0A
#define ICM42688_AODR_12_5Hz 0x0B
#define ICM42688_AODR_6_25Hz 0x0C
#define ICM42688_AODR_3_125Hz 0x0D
#define ICM42688_AODR_1_5625Hz 0x0E
#define ICM42688_AODR_500Hz 0x0F

View File

@@ -1,3 +0,0 @@
The files imu.c, imu.h, and ICM42688P are from https://github.com/flipperdevices/flipperzero-game-engine.git
Please see that file for the license.

View File

@@ -1,328 +0,0 @@
#include <furi.h>
#include "imu.h"
#include "ICM42688P/ICM42688P.h"
#define TAG "IMU"
#define ACCEL_GYRO_RATE DataRate100Hz
#define FILTER_SAMPLE_FREQ 100.f
#define FILTER_BETA 0.08f
#define SAMPLE_RATE_DIV 5
#define SENSITIVITY_K 30.f
#define EXP_RATE 1.1f
#define IMU_CALI_AVG 64
typedef enum {
ImuStop = (1 << 0),
ImuNewData = (1 << 1),
} ImuThreadFlags;
#define FLAGS_ALL (ImuStop | ImuNewData)
typedef struct {
float q0;
float q1;
float q2;
float q3;
float roll;
float pitch;
float yaw;
} ImuProcessedData;
typedef struct {
FuriThread* thread;
ICM42688P* icm42688p;
ImuProcessedData processed_data;
bool lefty;
} ImuThread;
static void imu_madgwick_filter(
ImuProcessedData* out,
ICM42688PScaledData* accel,
ICM42688PScaledData* gyro);
static void imu_irq_callback(void* context) {
furi_assert(context);
ImuThread* imu = context;
furi_thread_flags_set(furi_thread_get_id(imu->thread), ImuNewData);
}
static void imu_process_data(ImuThread* imu, ICM42688PFifoPacket* in_data) {
ICM42688PScaledData accel_data;
ICM42688PScaledData gyro_data;
// Get accel and gyro data in g and degrees/s
icm42688p_apply_scale_fifo(imu->icm42688p, in_data, &accel_data, &gyro_data);
// Gyro: degrees/s to rads/s
gyro_data.x = gyro_data.x / 180.f * M_PI;
gyro_data.y = gyro_data.y / 180.f * M_PI;
gyro_data.z = gyro_data.z / 180.f * M_PI;
// Sensor Fusion algorithm
ImuProcessedData* out = &imu->processed_data;
imu_madgwick_filter(out, &accel_data, &gyro_data);
// Quaternion to euler angles
float roll = atan2f(
out->q0 * out->q1 + out->q2 * out->q3, 0.5f - out->q1 * out->q1 - out->q2 * out->q2);
float pitch = asinf(-2.0f * (out->q1 * out->q3 - out->q0 * out->q2));
float yaw = atan2f(
out->q1 * out->q2 + out->q0 * out->q3, 0.5f - out->q2 * out->q2 - out->q3 * out->q3);
// Euler angles: rads to degrees
out->roll = roll / M_PI * 180.f;
out->pitch = pitch / M_PI * 180.f;
out->yaw = yaw / M_PI * 180.f;
}
static void calibrate_gyro(ImuThread* imu) {
ICM42688PRawData data;
ICM42688PScaledData offset_scaled = {.x = 0.f, .y = 0.f, .z = 0.f};
icm42688p_write_gyro_offset(imu->icm42688p, &offset_scaled);
furi_delay_ms(10);
int32_t avg_x = 0;
int32_t avg_y = 0;
int32_t avg_z = 0;
for(uint8_t i = 0; i < IMU_CALI_AVG; i++) {
icm42688p_read_gyro_raw(imu->icm42688p, &data);
avg_x += data.x;
avg_y += data.y;
avg_z += data.z;
furi_delay_ms(2);
}
data.x = avg_x / IMU_CALI_AVG;
data.y = avg_y / IMU_CALI_AVG;
data.z = avg_z / IMU_CALI_AVG;
icm42688p_apply_scale(&data, icm42688p_gyro_get_full_scale(imu->icm42688p), &offset_scaled);
FURI_LOG_I(
TAG,
"Offsets: x %f, y %f, z %f",
(double)offset_scaled.x,
(double)offset_scaled.y,
(double)offset_scaled.z);
icm42688p_write_gyro_offset(imu->icm42688p, &offset_scaled);
}
// static float imu_angle_diff(float a, float b) {
// float diff = a - b;
// if(diff > 180.f)
// diff -= 360.f;
// else if(diff < -180.f)
// diff += 360.f;
// return diff;
// }
static int32_t imu_thread(void* context) {
furi_assert(context);
ImuThread* imu = context;
// float yaw_last = 0.f;
// float pitch_last = 0.f;
// float diff_x = 0.f;
// float diff_y = 0.f;
calibrate_gyro(imu);
icm42688p_accel_config(imu->icm42688p, AccelFullScale16G, ACCEL_GYRO_RATE);
icm42688p_gyro_config(imu->icm42688p, GyroFullScale2000DPS, ACCEL_GYRO_RATE);
imu->processed_data.q0 = 1.f;
imu->processed_data.q1 = 0.f;
imu->processed_data.q2 = 0.f;
imu->processed_data.q3 = 0.f;
icm42688_fifo_enable(imu->icm42688p, imu_irq_callback, imu);
while(1) {
uint32_t events = furi_thread_flags_wait(FLAGS_ALL, FuriFlagWaitAny, FuriWaitForever);
if(events & ImuStop) {
break;
}
if(events & ImuNewData) {
uint16_t data_pending = icm42688_fifo_get_count(imu->icm42688p);
ICM42688PFifoPacket data;
while(data_pending--) {
icm42688_fifo_read(imu->icm42688p, &data);
imu_process_data(imu, &data);
}
}
}
icm42688_fifo_disable(imu->icm42688p);
return 0;
}
ImuThread* imu_start(ICM42688P* icm42688p) {
ImuThread* imu = malloc(sizeof(ImuThread));
imu->icm42688p = icm42688p;
imu->thread = furi_thread_alloc_ex("ImuThread", 4096, imu_thread, imu);
imu->lefty = furi_hal_rtc_is_flag_set(FuriHalRtcFlagHandOrient);
furi_thread_start(imu->thread);
return imu;
}
void imu_stop(ImuThread* imu) {
furi_assert(imu);
furi_thread_flags_set(furi_thread_get_id(imu->thread), ImuStop);
furi_thread_join(imu->thread);
furi_thread_free(imu->thread);
free(imu);
}
static float imu_inv_sqrt(float number) {
union {
float f;
uint32_t i;
} conv = {.f = number};
conv.i = 0x5F3759Df - (conv.i >> 1);
conv.f *= 1.5f - (number * 0.5f * conv.f * conv.f);
return conv.f;
}
/* Simple madgwik filter, based on: https://github.com/arduino-libraries/MadgwickAHRS/ */
static void imu_madgwick_filter(
ImuProcessedData* out,
ICM42688PScaledData* accel,
ICM42688PScaledData* gyro) {
float recipNorm;
float s0, s1, s2, s3;
float qDot1, qDot2, qDot3, qDot4;
float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2, _8q1, _8q2, q0q0, q1q1, q2q2, q3q3;
// Rate of change of quaternion from gyroscope
qDot1 = 0.5f * (-out->q1 * gyro->x - out->q2 * gyro->y - out->q3 * gyro->z);
qDot2 = 0.5f * (out->q0 * gyro->x + out->q2 * gyro->z - out->q3 * gyro->y);
qDot3 = 0.5f * (out->q0 * gyro->y - out->q1 * gyro->z + out->q3 * gyro->x);
qDot4 = 0.5f * (out->q0 * gyro->z + out->q1 * gyro->y - out->q2 * gyro->x);
// Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
if(!((accel->x == 0.0f) && (accel->y == 0.0f) && (accel->z == 0.0f))) {
// Normalise accelerometer measurement
recipNorm = imu_inv_sqrt(accel->x * accel->x + accel->y * accel->y + accel->z * accel->z);
accel->x *= recipNorm;
accel->y *= recipNorm;
accel->z *= recipNorm;
// Auxiliary variables to avoid repeated arithmetic
_2q0 = 2.0f * out->q0;
_2q1 = 2.0f * out->q1;
_2q2 = 2.0f * out->q2;
_2q3 = 2.0f * out->q3;
_4q0 = 4.0f * out->q0;
_4q1 = 4.0f * out->q1;
_4q2 = 4.0f * out->q2;
_8q1 = 8.0f * out->q1;
_8q2 = 8.0f * out->q2;
q0q0 = out->q0 * out->q0;
q1q1 = out->q1 * out->q1;
q2q2 = out->q2 * out->q2;
q3q3 = out->q3 * out->q3;
// Gradient decent algorithm corrective step
s0 = _4q0 * q2q2 + _2q2 * accel->x + _4q0 * q1q1 - _2q1 * accel->y;
s1 = _4q1 * q3q3 - _2q3 * accel->x + 4.0f * q0q0 * out->q1 - _2q0 * accel->y - _4q1 +
_8q1 * q1q1 + _8q1 * q2q2 + _4q1 * accel->z;
s2 = 4.0f * q0q0 * out->q2 + _2q0 * accel->x + _4q2 * q3q3 - _2q3 * accel->y - _4q2 +
_8q2 * q1q1 + _8q2 * q2q2 + _4q2 * accel->z;
s3 = 4.0f * q1q1 * out->q3 - _2q1 * accel->x + 4.0f * q2q2 * out->q3 - _2q2 * accel->y;
recipNorm =
imu_inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude
s0 *= recipNorm;
s1 *= recipNorm;
s2 *= recipNorm;
s3 *= recipNorm;
// Apply feedback step
qDot1 -= FILTER_BETA * s0;
qDot2 -= FILTER_BETA * s1;
qDot3 -= FILTER_BETA * s2;
qDot4 -= FILTER_BETA * s3;
}
// Integrate rate of change of quaternion to yield quaternion
out->q0 += qDot1 * (1.0f / FILTER_SAMPLE_FREQ);
out->q1 += qDot2 * (1.0f / FILTER_SAMPLE_FREQ);
out->q2 += qDot3 * (1.0f / FILTER_SAMPLE_FREQ);
out->q3 += qDot4 * (1.0f / FILTER_SAMPLE_FREQ);
// Normalise quaternion
recipNorm = imu_inv_sqrt(
out->q0 * out->q0 + out->q1 * out->q1 + out->q2 * out->q2 + out->q3 * out->q3);
out->q0 *= recipNorm;
out->q1 *= recipNorm;
out->q2 *= recipNorm;
out->q3 *= recipNorm;
}
/* IMU API */
struct Imu {
FuriHalSpiBusHandle* icm42688p_device;
ICM42688P* icm42688p;
ImuThread* thread;
bool present;
};
Imu* imu_alloc(void) {
Imu* imu = malloc(sizeof(Imu));
imu->icm42688p_device = malloc(sizeof(FuriHalSpiBusHandle));
memcpy(imu->icm42688p_device, &furi_hal_spi_bus_handle_external, sizeof(FuriHalSpiBusHandle));
imu->icm42688p_device->cs = &gpio_ext_pc3;
imu->icm42688p = icm42688p_alloc(imu->icm42688p_device, &gpio_ext_pb2);
imu->present = icm42688p_init(imu->icm42688p);
if(imu->present) {
imu->thread = imu_start(imu->icm42688p);
}
return imu;
}
void imu_free(Imu* imu) {
if(imu->present) {
imu_stop(imu->thread);
}
icm42688p_deinit(imu->icm42688p);
icm42688p_free(imu->icm42688p);
free(imu->icm42688p_device);
free(imu);
}
bool imu_present(Imu* imu) {
return imu->present;
}
float imu_pitch_get(Imu* imu) {
// we pretend that reading a float is an atomic operation
return imu->thread->lefty ? -imu->thread->processed_data.pitch :
imu->thread->processed_data.pitch;
}
float imu_roll_get(Imu* imu) {
// we pretend that reading a float is an atomic operation
return imu->thread->processed_data.roll;
}
float imu_yaw_get(Imu* imu) {
// we pretend that reading a float is an atomic operation
return imu->thread->lefty ? -imu->thread->processed_data.yaw : imu->thread->processed_data.yaw;
}

View File

@@ -1,15 +0,0 @@
#pragma once
typedef struct Imu Imu;
Imu* imu_alloc(void);
void imu_free(Imu* imu);
bool imu_present(Imu* imu);
float imu_pitch_get(Imu* imu);
float imu_roll_get(Imu* imu);
float imu_yaw_get(Imu* imu);

View File

@@ -1,161 +0,0 @@
#include "../../js_modules.h"
#include <furi.h>
#include <toolbox/path.h>
#include "imu.h"
#define TAG "JsVgm"
typedef struct {
Imu* imu;
bool present;
} JsVgmInst;
static void js_vgm_get_pitch(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsVgmInst* vgm = mjs_get_ptr(mjs, obj_inst);
furi_assert(vgm);
if(vgm->present) {
mjs_return(mjs, mjs_mk_number(mjs, imu_pitch_get(vgm->imu)));
} else {
FURI_LOG_T(TAG, "VGM not present.");
mjs_return(mjs, mjs_mk_undefined());
}
}
static void js_vgm_get_roll(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsVgmInst* vgm = mjs_get_ptr(mjs, obj_inst);
furi_assert(vgm);
if(vgm->present) {
mjs_return(mjs, mjs_mk_number(mjs, imu_roll_get(vgm->imu)));
} else {
FURI_LOG_T(TAG, "VGM not present.");
mjs_return(mjs, mjs_mk_undefined());
}
}
static void js_vgm_get_yaw(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsVgmInst* vgm = mjs_get_ptr(mjs, obj_inst);
furi_assert(vgm);
if(vgm->present) {
mjs_return(mjs, mjs_mk_number(mjs, imu_yaw_get(vgm->imu)));
} else {
FURI_LOG_T(TAG, "VGM not present.");
mjs_return(mjs, mjs_mk_undefined());
}
}
static float distance(float current, float start, float angle) {
// make 0 to 359.999...
current = (current < 0) ? current + 360 : current;
start = (start < 0) ? start + 360 : start;
// get max and min
bool max_is_current = current > start;
float max = (max_is_current) ? current : start;
float min = (max_is_current) ? start : current;
// get diff, check if it's greater than 180, and adjust
float diff = max - min;
bool diff_gt_180 = diff > 180;
if(diff_gt_180) {
diff = 360 - diff;
}
// if diff is less than angle return 0
if(diff < angle) {
FURI_LOG_T(TAG, "diff: %f, angle: %f", (double)diff, (double)angle);
return 0;
}
// return diff with sign
return (max_is_current ^ diff_gt_180) ? -diff : diff;
}
static void js_vgm_delta_yaw(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsVgmInst* vgm = mjs_get_ptr(mjs, obj_inst);
furi_assert(vgm);
size_t num_args = mjs_nargs(mjs);
if(num_args < 1 || num_args > 2) {
mjs_prepend_errorf(
mjs,
MJS_BAD_ARGS_ERROR,
"Invalid args. Pass (angle [, timeout]). Got %d args.",
num_args);
mjs_return(mjs, mjs_mk_undefined());
return;
}
if(!vgm->present) {
FURI_LOG_T(TAG, "VGM not present.");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
double angle = mjs_get_double(mjs, mjs_arg(mjs, 0));
if(isnan(angle)) {
mjs_prepend_errorf(mjs, MJS_TYPE_ERROR, "Invalid arg (angle).");
mjs_return(mjs, mjs_mk_undefined());
return;
}
uint32_t ms = (num_args == 2) ? mjs_get_int(mjs, mjs_arg(mjs, 1)) : 3000;
uint32_t timeout = furi_get_tick() + ms;
float initial_yaw = imu_yaw_get(vgm->imu);
do {
float current_yaw = imu_yaw_get(vgm->imu);
double diff = distance(current_yaw, initial_yaw, angle);
if(diff != 0) {
mjs_return(mjs, mjs_mk_number(mjs, diff));
return;
}
furi_delay_ms(100);
} while(furi_get_tick() < timeout);
mjs_return(mjs, mjs_mk_number(mjs, 0));
}
static void* js_vgm_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsVgmInst* vgm = malloc(sizeof(JsVgmInst));
vgm->imu = imu_alloc();
vgm->present = imu_present(vgm->imu);
if(!vgm->present) FURI_LOG_W(TAG, "VGM not present.");
mjs_val_t vgm_obj = mjs_mk_object(mjs);
mjs_set(mjs, vgm_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, vgm));
mjs_set(mjs, vgm_obj, "getPitch", ~0, MJS_MK_FN(js_vgm_get_pitch));
mjs_set(mjs, vgm_obj, "getRoll", ~0, MJS_MK_FN(js_vgm_get_roll));
mjs_set(mjs, vgm_obj, "getYaw", ~0, MJS_MK_FN(js_vgm_get_yaw));
mjs_set(mjs, vgm_obj, "deltaYaw", ~0, MJS_MK_FN(js_vgm_delta_yaw));
*object = vgm_obj;
return vgm;
}
static void js_vgm_destroy(void* inst) {
JsVgmInst* vgm = inst;
furi_assert(vgm);
imu_free(vgm->imu);
vgm->imu = NULL;
free(vgm);
}
static const JsModuleDescriptor js_vgm_desc = {
"vgm",
js_vgm_create,
js_vgm_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_vgm_desc,
};
const FlipperAppPluginDescriptor* js_vgm_ep(void) {
return &plugin_descriptor;
}

View File

@@ -1,24 +0,0 @@
# Flipper Unleashed Firmware JavaScript SDK Wizard
This package contains an interactive wizard that lets you scaffold a JavaScript
application for Flipper Zero using the Custom Unleashed Firmware JS SDK.
This is a fork of the [Official Flipper Zero JS SDK Wizard](https://www.npmjs.com/package/@flipperdevices/create-fz-app),
configured to use the [Unleashed FW JavaScript SDK]((https://www.npmjs.com/package/@darkflippers/fz-sdk-ul)) instead.
No other changes are included.
## Getting started
Create your application using the interactive wizard:
```shell
npx @darkflippers/create-fz-app-ul@latest
```
Then, enter the directory with your application and launch it:
```shell
cd my-flip-app
npm start
```
You are free to use `pnpm` or `yarn` instead of `npm`.
## Documentation
Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html)

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env node
import prompts from "prompts";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "url";
import { spawnSync } from "node:child_process";
import { replaceInFileSync } from "replace-in-file";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
(async () => {
const { name, pkgManager, confirm } = await prompts([
{
type: "text",
name: "name",
message: "What is the name of your project?",
initial: "my-flip-app"
},
{
type: "select",
name: "pkgManager",
message: "What package manager should your project use?",
choices: [
{ title: "npm", value: "npm" },
{ title: "pnpm", value: "pnpm" },
{ title: "yarn", value: "yarn" },
],
},
{
type: "confirm",
name: "confirm",
message: "Create project?",
initial: true,
},
]);
if (!confirm)
return;
if (fs.existsSync(name)) {
const { replace } = await prompts([
{
type: "confirm",
name: "replace",
message: `File or directory \`${name}\` already exists. Continue anyway?`,
initial: false,
},
]);
if (!replace)
return;
}
fs.rmSync(name, { recursive: true, force: true });
console.log("Copying files...");
fs.cpSync(path.resolve(__dirname, "template"), name, { recursive: true });
replaceInFileSync({ files: `${name}/**/*`, from: /<app_name>/g, to: name });
console.log("Installing packages...");
spawnSync("bash", ["-c", `cd ${name} && ${pkgManager} install`], {
cwd: process.cwd(),
detached: true,
stdio: "inherit",
});
console.log(`Done! Created ${name}. Run \`cd ${name} && ${pkgManager} start\` to run it on your Flipper.`);
})();

Some files were not shown because too many files have changed in this diff Show More