mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2025-12-12 20:49:49 +04:00
merge p2
This commit is contained in:
@@ -16,11 +16,70 @@ App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="js_dialog",
|
appid="js_event_loop",
|
||||||
apptype=FlipperAppType.PLUGIN,
|
apptype=FlipperAppType.PLUGIN,
|
||||||
entry_point="js_dialog_ep",
|
entry_point="js_event_loop_ep",
|
||||||
requires=["js_app"],
|
requires=["js_app"],
|
||||||
sources=["modules/js_dialog.c"],
|
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", "js_event_loop"],
|
||||||
|
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", "js_gui", "js_event_loop"],
|
||||||
|
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", "js_gui", "js_event_loop"],
|
||||||
|
sources=["modules/js_gui/empty_screen.c"],
|
||||||
|
)
|
||||||
|
|
||||||
|
App(
|
||||||
|
appid="js_gui__submenu",
|
||||||
|
apptype=FlipperAppType.PLUGIN,
|
||||||
|
entry_point="js_view_submenu_ep",
|
||||||
|
requires=["js_app", "js_gui"],
|
||||||
|
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", "js_gui", "js_event_loop"],
|
||||||
|
sources=["modules/js_gui/text_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(
|
App(
|
||||||
@@ -48,35 +107,11 @@ App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="js_storage",
|
appid="js_gpio",
|
||||||
apptype=FlipperAppType.PLUGIN,
|
apptype=FlipperAppType.PLUGIN,
|
||||||
entry_point="js_storage_ep",
|
entry_point="js_gpio_ep",
|
||||||
requires=["js_app"],
|
requires=["js_app", "js_event_loop"],
|
||||||
sources=["modules/js_storage.c"],
|
sources=["modules/js_gpio.c"],
|
||||||
)
|
|
||||||
|
|
||||||
App(
|
|
||||||
appid="js_usbdisk",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_usbdisk_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_usbdisk/*.c"],
|
|
||||||
)
|
|
||||||
|
|
||||||
App(
|
|
||||||
appid="js_submenu",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_submenu_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_submenu.c"],
|
|
||||||
)
|
|
||||||
|
|
||||||
App(
|
|
||||||
appid="js_blebeacon",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_blebeacon_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_blebeacon.c"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
@@ -88,48 +123,9 @@ App(
|
|||||||
)
|
)
|
||||||
|
|
||||||
App(
|
App(
|
||||||
appid="js_keyboard",
|
appid="js_storage",
|
||||||
apptype=FlipperAppType.PLUGIN,
|
apptype=FlipperAppType.PLUGIN,
|
||||||
entry_point="js_keyboard_ep",
|
entry_point="js_storage_ep",
|
||||||
requires=["js_app"],
|
requires=["js_app"],
|
||||||
sources=["modules/js_keyboard.c"],
|
sources=["modules/js_storage.c"],
|
||||||
)
|
|
||||||
App(
|
|
||||||
appid="js_subghz",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_subghz_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_subghz/*.c"],
|
|
||||||
)
|
|
||||||
|
|
||||||
App(
|
|
||||||
appid="js_gpio",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_gpio_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_gpio.c"],
|
|
||||||
)
|
|
||||||
|
|
||||||
App(
|
|
||||||
appid="js_textbox",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_textbox_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_textbox.c"],
|
|
||||||
)
|
|
||||||
|
|
||||||
App(
|
|
||||||
appid="js_widget",
|
|
||||||
apptype=FlipperAppType.PLUGIN,
|
|
||||||
entry_point="js_widget_ep",
|
|
||||||
requires=["js_app"],
|
|
||||||
sources=["modules/js_widget.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"],
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ int32_t js_app(void* arg) {
|
|||||||
FuriString* start_text =
|
FuriString* start_text =
|
||||||
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
|
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_get_cstr(start_text));
|
||||||
console_view_print(app->console_view, "------------");
|
console_view_print(app->console_view, "-------------");
|
||||||
furi_string_free(name);
|
furi_string_free(name);
|
||||||
furi_string_free(start_text);
|
furi_string_free(start_text);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#include <core/common_defines.h>
|
#include <core/common_defines.h>
|
||||||
#include "js_modules.h"
|
#include "js_modules.h"
|
||||||
#include <m-dict.h>
|
#include <m-array.h>
|
||||||
|
|
||||||
#include "modules/js_flipper.h"
|
#include "modules/js_flipper.h"
|
||||||
|
#ifdef FW_CFG_unit_tests
|
||||||
|
#include "modules/js_tests.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define TAG "JS modules"
|
#define TAG "JS modules"
|
||||||
|
|
||||||
@@ -9,54 +13,72 @@
|
|||||||
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
|
#define MODULES_PATH "/ext/apps_data/js_app/plugins"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
JsModeConstructor create;
|
FuriString* name;
|
||||||
JsModeDestructor destroy;
|
const JsModuleConstructor create;
|
||||||
|
const JsModuleDestructor destroy;
|
||||||
void* context;
|
void* context;
|
||||||
} JsModuleData;
|
} JsModuleData;
|
||||||
|
|
||||||
DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST);
|
// 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);
|
||||||
|
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
|
||||||
|
|
||||||
static const JsModuleDescriptor modules_builtin[] = {
|
static const JsModuleDescriptor modules_builtin[] = {
|
||||||
{"flipper", js_flipper_create, NULL},
|
{"flipper", js_flipper_create, NULL, NULL},
|
||||||
|
#ifdef FW_CFG_unit_tests
|
||||||
|
{"tests", js_tests_create, NULL, NULL},
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
struct JsModules {
|
struct JsModules {
|
||||||
struct mjs* mjs;
|
struct mjs* mjs;
|
||||||
JsModuleDict_t module_dict;
|
JsModuleArray_t modules;
|
||||||
PluginManager* plugin_manager;
|
PluginManager* plugin_manager;
|
||||||
|
CompositeApiResolver* resolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
|
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
|
||||||
JsModules* modules = malloc(sizeof(JsModules));
|
JsModules* modules = malloc(sizeof(JsModules));
|
||||||
modules->mjs = mjs;
|
modules->mjs = mjs;
|
||||||
JsModuleDict_init(modules->module_dict);
|
JsModuleArray_init(modules->modules);
|
||||||
|
|
||||||
modules->plugin_manager = plugin_manager_alloc(
|
modules->plugin_manager = plugin_manager_alloc(
|
||||||
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
|
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
|
||||||
|
|
||||||
|
modules->resolver = resolver;
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
void js_modules_destroy(JsModules* modules) {
|
void js_modules_destroy(JsModules* instance) {
|
||||||
JsModuleDict_it_t it;
|
for
|
||||||
for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it);
|
M_EACH(module, instance->modules, JsModuleArray_t) {
|
||||||
JsModuleDict_next(it)) {
|
FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name));
|
||||||
const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it);
|
if(module->destroy) module->destroy(module->context);
|
||||||
if(module_itref->value.destroy) {
|
furi_string_free(module->name);
|
||||||
module_itref->value.destroy(module_itref->value.context);
|
|
||||||
}
|
}
|
||||||
}
|
plugin_manager_free(instance->plugin_manager);
|
||||||
plugin_manager_free(modules->plugin_manager);
|
JsModuleArray_clear(instance->modules);
|
||||||
JsModuleDict_clear(modules->module_dict);
|
free(instance);
|
||||||
free(modules);
|
}
|
||||||
|
|
||||||
|
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) {
|
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) {
|
||||||
FuriString* module_name = furi_string_alloc_set_str(name);
|
|
||||||
// Check if module is already installed
|
// Check if module is already installed
|
||||||
JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
JsModuleData* module_inst = js_find_loaded_module(modules, name);
|
||||||
if(module_inst) { //-V547
|
if(module_inst) { //-V547
|
||||||
furi_string_free(module_name);
|
|
||||||
mjs_prepend_errorf(
|
mjs_prepend_errorf(
|
||||||
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
|
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
|
||||||
return MJS_UNDEFINED;
|
return MJS_UNDEFINED;
|
||||||
@@ -73,8 +95,11 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
|||||||
|
|
||||||
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
|
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
|
||||||
JsModuleData module = {
|
JsModuleData module = {
|
||||||
.create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy};
|
.create = modules_builtin[i].create,
|
||||||
JsModuleDict_set_at(modules->module_dict, module_name, module);
|
.destroy = modules_builtin[i].destroy,
|
||||||
|
.name = furi_string_alloc_set_str(name),
|
||||||
|
};
|
||||||
|
JsModuleArray_push_at(modules->modules, 0, module);
|
||||||
module_found = true;
|
module_found = true;
|
||||||
FURI_LOG_I(TAG, "Using built-in module %s", name);
|
FURI_LOG_I(TAG, "Using built-in module %s", name);
|
||||||
break;
|
break;
|
||||||
@@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
|||||||
|
|
||||||
// External module load
|
// External module load
|
||||||
if(!module_found) {
|
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();
|
FuriString* module_path = furi_string_alloc();
|
||||||
furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name);
|
furi_string_printf(
|
||||||
FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path));
|
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 {
|
do {
|
||||||
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
|
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
|
||||||
PluginManagerError load_error = plugin_manager_load_single(
|
PluginManagerError load_error = plugin_manager_load_single(
|
||||||
modules->plugin_manager, furi_string_get_cstr(module_path));
|
modules->plugin_manager, furi_string_get_cstr(module_path));
|
||||||
if(load_error != PluginManagerErrorNone) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
const JsModuleDescriptor* plugin =
|
const JsModuleDescriptor* plugin =
|
||||||
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
|
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
|
||||||
furi_assert(plugin);
|
furi_assert(plugin);
|
||||||
|
|
||||||
if(strncmp(name, plugin->name, name_len) != 0) {
|
if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) {
|
||||||
FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name);
|
FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy};
|
JsModuleData module = {
|
||||||
JsModuleDict_set_at(modules->module_dict, module_name, 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;
|
module_found = true;
|
||||||
} while(0);
|
} while(0);
|
||||||
furi_string_free(module_path);
|
furi_string_free(module_path);
|
||||||
|
furi_string_free(deslashed_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run module constructor
|
// Run module constructor
|
||||||
mjs_val_t module_object = MJS_UNDEFINED;
|
mjs_val_t module_object = MJS_UNDEFINED;
|
||||||
if(module_found) {
|
if(module_found) {
|
||||||
module_inst = JsModuleDict_get(modules->module_dict, module_name);
|
module_inst = js_find_loaded_module(modules, name);
|
||||||
furi_assert(module_inst);
|
furi_assert(module_inst);
|
||||||
if(module_inst->create) { //-V779
|
if(module_inst->create) { //-V779
|
||||||
module_inst->context = module_inst->create(modules->mjs, &module_object);
|
module_inst->context = module_inst->create(modules->mjs, &module_object, modules);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +166,12 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le
|
|||||||
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
|
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
furi_string_free(module_name);
|
|
||||||
|
|
||||||
return module_object;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include "js_thread_i.h"
|
#include "js_thread_i.h"
|
||||||
#include <flipper_application/flipper_application.h>
|
#include <flipper_application/flipper_application.h>
|
||||||
#include <flipper_application/plugins/plugin_manager.h>
|
#include <flipper_application/plugins/plugin_manager.h>
|
||||||
@@ -7,19 +9,269 @@
|
|||||||
#define PLUGIN_APP_ID "js"
|
#define PLUGIN_APP_ID "js"
|
||||||
#define PLUGIN_API_VERSION 1
|
#define PLUGIN_API_VERSION 1
|
||||||
|
|
||||||
typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object);
|
/**
|
||||||
typedef void (*JsModeDestructor)(void* inst);
|
* @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
|
||||||
|
* ```c
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// Are you tired of your silly little JS+C glue code functions being 75%
|
||||||
|
// argument validation code and 25% actual logic? Introducing: ASS (Argument
|
||||||
|
// Schema for Scripts)! ASS is a set of macros that reduce the typical
|
||||||
|
// boilerplate code of "check argument count, get arguments, validate arguments,
|
||||||
|
// extract C values from arguments" down to just one line!
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
|
||||||
|
* that the function requires exactly as many arguments as were specified.
|
||||||
|
*/
|
||||||
|
#define JS_EXACTLY ==
|
||||||
|
/**
|
||||||
|
* When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies
|
||||||
|
* that the function requires at least as many arguments as were specified.
|
||||||
|
*/
|
||||||
|
#define JS_AT_LEAST >=
|
||||||
|
|
||||||
|
#define JS_ENUM_MAP(var_name, ...) \
|
||||||
|
static const JsEnumMapping var_name##_mapping[] = { \
|
||||||
|
{NULL, sizeof(var_name)}, \
|
||||||
|
__VA_ARGS__, \
|
||||||
|
{NULL, 0}, \
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char* name;
|
||||||
|
size_t value;
|
||||||
|
} JsEnumMapping;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void* out;
|
||||||
|
int (*validator)(mjs_val_t);
|
||||||
|
void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra);
|
||||||
|
const char* expected_type;
|
||||||
|
bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra);
|
||||||
|
const void* extra_data;
|
||||||
|
} _js_arg_decl;
|
||||||
|
|
||||||
|
static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||||
|
UNUSED(extra);
|
||||||
|
*(int32_t*)out = mjs_get_int32(mjs, *in);
|
||||||
|
}
|
||||||
|
#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL})
|
||||||
|
|
||||||
|
static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||||
|
UNUSED(extra);
|
||||||
|
*(void**)out = mjs_get_ptr(mjs, *in);
|
||||||
|
}
|
||||||
|
#define JS_ARG_PTR(out) \
|
||||||
|
((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL})
|
||||||
|
|
||||||
|
static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||||
|
UNUSED(extra);
|
||||||
|
*(const char**)out = mjs_get_string(mjs, in, NULL);
|
||||||
|
}
|
||||||
|
#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL})
|
||||||
|
|
||||||
|
static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||||
|
UNUSED(extra);
|
||||||
|
*(bool*)out = !!mjs_get_bool(mjs, *in);
|
||||||
|
}
|
||||||
|
#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL})
|
||||||
|
|
||||||
|
static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) {
|
||||||
|
UNUSED(extra);
|
||||||
|
UNUSED(mjs);
|
||||||
|
*(mjs_val_t*)out = *in;
|
||||||
|
}
|
||||||
|
#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL})
|
||||||
|
#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL})
|
||||||
|
#define JS_ARG_FN(out) \
|
||||||
|
((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL})
|
||||||
|
#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL})
|
||||||
|
|
||||||
|
static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||||
|
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
|
||||||
|
JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val);
|
||||||
|
return struct_magic == expected_magic;
|
||||||
|
}
|
||||||
|
#define JS_ARG_STRUCT(type, out) \
|
||||||
|
((_js_arg_decl){ \
|
||||||
|
out, \
|
||||||
|
mjs_is_foreign, \
|
||||||
|
_js_to_ptr, \
|
||||||
|
#type, \
|
||||||
|
_js_validate_struct, \
|
||||||
|
(void*)JsForeignMagic##_##type})
|
||||||
|
|
||||||
|
static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||||
|
JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra;
|
||||||
|
JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val);
|
||||||
|
return struct_magic == expected_magic;
|
||||||
|
}
|
||||||
|
#define JS_ARG_OBJ_WITH_STRUCT(type, out) \
|
||||||
|
((_js_arg_decl){ \
|
||||||
|
out, \
|
||||||
|
mjs_is_object, \
|
||||||
|
_js_passthrough, \
|
||||||
|
#type, \
|
||||||
|
_js_validate_obj_w_struct, \
|
||||||
|
(void*)JsForeignMagic##_##type})
|
||||||
|
|
||||||
|
static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) {
|
||||||
|
for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++)
|
||||||
|
if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static inline void
|
||||||
|
_js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) {
|
||||||
|
const JsEnumMapping* mapping = (JsEnumMapping*)extra;
|
||||||
|
size_t size = mapping->value; // get enum size from first entry
|
||||||
|
for(mapping++; mapping->name; mapping++) {
|
||||||
|
if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) {
|
||||||
|
if(size == 1)
|
||||||
|
*(uint8_t*)out = mapping->value;
|
||||||
|
else if(size == 2)
|
||||||
|
*(uint16_t*)out = mapping->value;
|
||||||
|
else if(size == 4)
|
||||||
|
*(uint32_t*)out = mapping->value;
|
||||||
|
else if(size == 8)
|
||||||
|
*(uint64_t*)out = mapping->value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// unreachable, thanks to _js_validate_enum
|
||||||
|
}
|
||||||
|
#define JS_ARG_ENUM(var_name, name) \
|
||||||
|
((_js_arg_decl){ \
|
||||||
|
&var_name, \
|
||||||
|
mjs_is_string, \
|
||||||
|
_js_convert_enum, \
|
||||||
|
name " enum", \
|
||||||
|
_js_validate_enum, \
|
||||||
|
var_name##_mapping})
|
||||||
|
|
||||||
|
//-V:JS_FETCH_ARGS_OR_RETURN:1008
|
||||||
|
/**
|
||||||
|
* @brief Fetches and validates the arguments passed to a JS function
|
||||||
|
*
|
||||||
|
* Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));`
|
||||||
|
*
|
||||||
|
* @warning This macro executes `return;` by design in case of an argument count
|
||||||
|
* mismatch or a validation failure
|
||||||
|
*/
|
||||||
|
#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \
|
||||||
|
_js_arg_decl _js_args[] = {__VA_ARGS__}; \
|
||||||
|
int _js_arg_cnt = COUNT_OF(_js_args); \
|
||||||
|
mjs_val_t _js_arg_vals[_js_arg_cnt]; \
|
||||||
|
if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \
|
||||||
|
JS_ERROR_AND_RETURN( \
|
||||||
|
mjs, \
|
||||||
|
MJS_BAD_ARGS_ERROR, \
|
||||||
|
"expected %s%d arguments, got %d", \
|
||||||
|
#arg_operator, \
|
||||||
|
_js_arg_cnt, \
|
||||||
|
mjs_nargs(mjs)); \
|
||||||
|
for(int _i = 0; _i < _js_arg_cnt; _i++) { \
|
||||||
|
_js_arg_vals[_i] = mjs_arg(mjs, _i); \
|
||||||
|
if(_js_args[_i].validator) \
|
||||||
|
if(!_js_args[_i].validator(_js_arg_vals[_i])) \
|
||||||
|
JS_ERROR_AND_RETURN( \
|
||||||
|
mjs, \
|
||||||
|
MJS_BAD_ARGS_ERROR, \
|
||||||
|
"argument %d: expected %s", \
|
||||||
|
_i, \
|
||||||
|
_js_args[_i].expected_type); \
|
||||||
|
if(_js_args[_i].extended_validator) \
|
||||||
|
if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \
|
||||||
|
JS_ERROR_AND_RETURN( \
|
||||||
|
mjs, \
|
||||||
|
MJS_BAD_ARGS_ERROR, \
|
||||||
|
"argument %d: expected %s", \
|
||||||
|
_i, \
|
||||||
|
_js_args[_i].expected_type); \
|
||||||
|
_js_args[_i].converter( \
|
||||||
|
mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
|
||||||
|
typedef struct JsModules JsModules;
|
||||||
|
|
||||||
|
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||||
|
typedef void (*JsModuleDestructor)(void* inst);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char* name;
|
char* name;
|
||||||
JsModeConstructor create;
|
JsModuleConstructor create;
|
||||||
JsModeDestructor destroy;
|
JsModuleDestructor destroy;
|
||||||
|
const ElfApiInterface* api_interface;
|
||||||
} JsModuleDescriptor;
|
} JsModuleDescriptor;
|
||||||
|
|
||||||
typedef struct JsModules JsModules;
|
|
||||||
|
|
||||||
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
|
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
|
||||||
|
|
||||||
void js_modules_destroy(JsModules* modules);
|
void js_modules_destroy(JsModules* modules);
|
||||||
|
|
||||||
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);
|
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);
|
||||||
|
|||||||
@@ -196,17 +196,11 @@ static void js_require(struct mjs* mjs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void js_global_to_string(struct mjs* mjs) {
|
static void js_global_to_string(struct mjs* mjs) {
|
||||||
double num = mjs_get_double(mjs, mjs_arg(mjs, 0));
|
int base = 10;
|
||||||
char tmp_str[] = "-2147483648";
|
if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1));
|
||||||
itoa(num, tmp_str, 10);
|
|
||||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
|
||||||
mjs_return(mjs, ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_global_to_hex_string(struct mjs* mjs) {
|
|
||||||
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
|
double num = mjs_get_int(mjs, mjs_arg(mjs, 0));
|
||||||
char tmp_str[] = "-FFFFFFFF";
|
char tmp_str[] = "-2147483648";
|
||||||
itoa(num, tmp_str, 16);
|
itoa(num, tmp_str, base);
|
||||||
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true);
|
||||||
mjs_return(mjs, ret);
|
mjs_return(mjs, ret);
|
||||||
}
|
}
|
||||||
@@ -340,8 +334,7 @@ static int32_t js_thread(void* arg) {
|
|||||||
}
|
}
|
||||||
mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print));
|
mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print));
|
||||||
mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay));
|
mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay));
|
||||||
mjs_set(mjs, global, "to_string", ~0, MJS_MK_FN(js_global_to_string));
|
mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string));
|
||||||
mjs_set(mjs, global, "to_hex_string", ~0, MJS_MK_FN(js_global_to_hex_string));
|
|
||||||
mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
|
mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address));
|
||||||
mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require));
|
mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require));
|
||||||
mjs_set(mjs, global, "parse_int", ~0, MJS_MK_FN(js_parse_int));
|
mjs_set(mjs, global, "parse_int", ~0, MJS_MK_FN(js_parse_int));
|
||||||
@@ -400,8 +393,8 @@ static int32_t js_thread(void* arg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
js_modules_destroy(worker->modules);
|
|
||||||
mjs_destroy(mjs);
|
mjs_destroy(mjs);
|
||||||
|
js_modules_destroy(worker->modules);
|
||||||
|
|
||||||
composite_api_resolver_free(worker->resolver);
|
composite_api_resolver_free(worker->resolver);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct JsThread JsThread;
|
typedef struct JsThread JsThread;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -14,3 +18,7 @@ typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* con
|
|||||||
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
|
JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context);
|
||||||
|
|
||||||
void js_thread_stop(JsThread* worker);
|
void js_thread_stop(JsThread* worker);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ static bool setup_parse_params(
|
|||||||
}
|
}
|
||||||
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
|
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 pid_obj = mjs_get(mjs, arg, "pid", ~0);
|
||||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0);
|
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
|
||||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0);
|
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
|
||||||
mjs_val_t layout_obj = mjs_get(mjs, arg, "layout_path", ~0);
|
mjs_val_t layout_obj = mjs_get(mjs, arg, "layout_path", ~0);
|
||||||
|
|
||||||
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
||||||
@@ -486,7 +486,8 @@ static void js_badusb_alt_println(struct mjs* mjs) {
|
|||||||
badusb_print(mjs, true, true);
|
badusb_print(mjs, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) {
|
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
|
UNUSED(modules);
|
||||||
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
|
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
|
||||||
mjs_val_t badusb_obj = mjs_mk_object(mjs);
|
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, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
|
||||||
@@ -514,6 +515,7 @@ static const JsModuleDescriptor js_badusb_desc = {
|
|||||||
"badusb",
|
"badusb",
|
||||||
js_badusb_create,
|
js_badusb_create,
|
||||||
js_badusb_destroy,
|
js_badusb_destroy,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
#include <core/common_defines.h>
|
|
||||||
#include "../js_modules.h"
|
|
||||||
#include <dialogs/dialogs.h>
|
|
||||||
|
|
||||||
// File icon
|
|
||||||
#include <gui/icon_i.h>
|
|
||||||
static const uint8_t _I_file_10px_0[] = {
|
|
||||||
0x00, 0x7f, 0x00, 0xa1, 0x00, 0x2d, 0x01, 0xe1, 0x01, 0x0d, 0x01,
|
|
||||||
0x01, 0x01, 0x7d, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0x01,
|
|
||||||
};
|
|
||||||
static const uint8_t* const _I_file_10px[] = {_I_file_10px_0};
|
|
||||||
|
|
||||||
static const Icon I_file_10px =
|
|
||||||
{.width = 10, .height = 10, .frame_count = 1, .frame_rate = 0, .frames = _I_file_10px};
|
|
||||||
// File icon end
|
|
||||||
|
|
||||||
static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) {
|
|
||||||
size_t num_args = mjs_nargs(mjs);
|
|
||||||
if(num_args != 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mjs_val_t header_obj = mjs_arg(mjs, 0);
|
|
||||||
mjs_val_t msg_obj = mjs_arg(mjs, 1);
|
|
||||||
if((!mjs_is_string(header_obj)) || (!mjs_is_string(msg_obj))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t arg_len = 0;
|
|
||||||
*hdr = mjs_get_string(mjs, &header_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
*hdr = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
*msg = mjs_get_string(mjs, &msg_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
*msg = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_dialog_message(struct mjs* mjs) {
|
|
||||||
const char* dialog_header = NULL;
|
|
||||||
const char* dialog_msg = NULL;
|
|
||||||
if(!js_dialog_msg_parse_params(mjs, &dialog_header, &dialog_msg)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
|
||||||
DialogMessage* message = dialog_message_alloc();
|
|
||||||
dialog_message_set_buttons(message, NULL, "OK", NULL);
|
|
||||||
if(dialog_header) {
|
|
||||||
dialog_message_set_header(message, dialog_header, 64, 3, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
if(dialog_msg) {
|
|
||||||
dialog_message_set_text(message, dialog_msg, 64, 26, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
|
||||||
dialog_message_free(message);
|
|
||||||
furi_record_close(RECORD_DIALOGS);
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, result == DialogMessageButtonCenter));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_dialog_custom(struct mjs* mjs) {
|
|
||||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
|
||||||
DialogMessage* message = dialog_message_alloc();
|
|
||||||
|
|
||||||
bool params_correct = false;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if(mjs_nargs(mjs) != 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
mjs_val_t params_obj = mjs_arg(mjs, 0);
|
|
||||||
if(!mjs_is_object(params_obj)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t text_obj = mjs_get(mjs, params_obj, "header", ~0);
|
|
||||||
size_t arg_len = 0;
|
|
||||||
const char* text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
text_str = NULL;
|
|
||||||
}
|
|
||||||
if(text_str) {
|
|
||||||
dialog_message_set_header(message, text_str, 64, 3, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
text_obj = mjs_get(mjs, params_obj, "text", ~0);
|
|
||||||
text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
text_str = NULL;
|
|
||||||
}
|
|
||||||
if(text_str) {
|
|
||||||
dialog_message_set_text(message, text_str, 64, 26, AlignCenter, AlignTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t btn_obj[3] = {
|
|
||||||
mjs_get(mjs, params_obj, "button_left", ~0),
|
|
||||||
mjs_get(mjs, params_obj, "button_center", ~0),
|
|
||||||
mjs_get(mjs, params_obj, "button_right", ~0),
|
|
||||||
};
|
|
||||||
const char* btn_text[3] = {NULL, NULL, NULL};
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < 3; i++) {
|
|
||||||
if(!mjs_is_string(btn_obj[i])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
btn_text[i] = mjs_get_string(mjs, &btn_obj[i], &arg_len);
|
|
||||||
if(arg_len == 0) {
|
|
||||||
btn_text[i] = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog_message_set_buttons(message, btn_text[0], btn_text[1], btn_text[2]);
|
|
||||||
|
|
||||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
|
||||||
mjs_val_t return_obj = MJS_UNDEFINED;
|
|
||||||
if(result == DialogMessageButtonLeft) {
|
|
||||||
return_obj = mjs_mk_string(mjs, btn_text[0], ~0, true);
|
|
||||||
} else if(result == DialogMessageButtonCenter) {
|
|
||||||
return_obj = mjs_mk_string(mjs, btn_text[1], ~0, true);
|
|
||||||
} else if(result == DialogMessageButtonRight) {
|
|
||||||
return_obj = mjs_mk_string(mjs, btn_text[2], ~0, true);
|
|
||||||
} else {
|
|
||||||
return_obj = mjs_mk_string(mjs, "", ~0, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_return(mjs, return_obj);
|
|
||||||
params_correct = true;
|
|
||||||
} while(0);
|
|
||||||
|
|
||||||
dialog_message_free(message);
|
|
||||||
furi_record_close(RECORD_DIALOGS);
|
|
||||||
|
|
||||||
if(!params_correct) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_dialog_pick_file(struct mjs* mjs) {
|
|
||||||
if(mjs_nargs(mjs) != 2) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Wrong arguments");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t base_path_obj = mjs_arg(mjs, 0);
|
|
||||||
if(!mjs_is_string(base_path_obj)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base path must be a string");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
size_t base_path_len = 0;
|
|
||||||
const char* base_path = mjs_get_string(mjs, &base_path_obj, &base_path_len);
|
|
||||||
if((base_path_len == 0) || (base_path == NULL)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad base path argument");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t extension_obj = mjs_arg(mjs, 1);
|
|
||||||
if(!mjs_is_string(extension_obj)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Extension must be a string");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
size_t extension_len = 0;
|
|
||||||
const char* extension = mjs_get_string(mjs, &extension_obj, &extension_len);
|
|
||||||
if((extension_len == 0) || (extension == NULL)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad extension argument");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_dialog_create(struct mjs* mjs, mjs_val_t* object) {
|
|
||||||
mjs_val_t dialog_obj = mjs_mk_object(mjs);
|
|
||||||
mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message));
|
|
||||||
mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom));
|
|
||||||
mjs_set(mjs, dialog_obj, "pickFile", ~0, MJS_MK_FN(js_dialog_pick_file));
|
|
||||||
*object = dialog_obj;
|
|
||||||
|
|
||||||
return (void*)1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const JsModuleDescriptor js_dialog_desc = {
|
|
||||||
"dialog",
|
|
||||||
js_dialog_create,
|
|
||||||
NULL,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
|
||||||
.appid = PLUGIN_APP_ID,
|
|
||||||
.ep_api_version = PLUGIN_API_VERSION,
|
|
||||||
.entry_point = &js_dialog_desc,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlipperAppPluginDescriptor* js_dialog_ep(void) {
|
|
||||||
return &plugin_descriptor;
|
|
||||||
}
|
|
||||||
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
#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 {
|
||||||
|
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;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriEventLoop* loop;
|
||||||
|
struct mjs* mjs;
|
||||||
|
} JsEventLoopTickContext;
|
||||||
|
|
||||||
|
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
|
||||||
|
JsEventLoopTickContext* tick_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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_apply(
|
||||||
|
context->mjs,
|
||||||
|
&result,
|
||||||
|
context->callback,
|
||||||
|
MJS_UNDEFINED,
|
||||||
|
context->arity,
|
||||||
|
context->arguments);
|
||||||
|
|
||||||
|
// 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 bool 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);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
||||||
|
furi_event_loop_timer_stop(subscription->object);
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
JsEventLoopContract* contract;
|
||||||
|
mjs_val_t callback;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(
|
||||||
|
mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&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->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) {
|
||||||
|
// get arguments
|
||||||
|
const char* mode_str;
|
||||||
|
int32_t interval;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval));
|
||||||
|
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||||
|
|
||||||
|
FuriEventLoopTimerType mode;
|
||||||
|
if(strcasecmp(mode_str, "periodic") == 0) {
|
||||||
|
mode = FuriEventLoopTimerTypePeriodic;
|
||||||
|
} else if(strcasecmp(mode_str, "oneshot") == 0) {
|
||||||
|
mode = FuriEventLoopTimerTypeOnce;
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
mjs_val_t message;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&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
|
||||||
|
int32_t length;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&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_tick(void* param) {
|
||||||
|
JsEventLoopTickContext* context = param;
|
||||||
|
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
|
||||||
|
if(flags & FuriFlagError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(flags & ThreadEventStop) {
|
||||||
|
furi_event_loop_stop(context->loop);
|
||||||
|
mjs_exit(context->mjs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext));
|
||||||
|
module->loop = furi_event_loop_alloc();
|
||||||
|
tick_ctx->loop = module->loop;
|
||||||
|
tick_ctx->mjs = mjs;
|
||||||
|
module->tick_context = tick_ctx;
|
||||||
|
furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx);
|
||||||
|
SubscriptionArray_init(module->subscriptions);
|
||||||
|
ContractArray_init(module->owned_contracts);
|
||||||
|
|
||||||
|
mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||||
|
mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe));
|
||||||
|
mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run));
|
||||||
|
mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop));
|
||||||
|
mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer));
|
||||||
|
mjs_set(mjs, event_loop_obj, "queue", ~0, 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->tick_context);
|
||||||
|
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;
|
||||||
|
}
|
||||||
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#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(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#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*))));
|
||||||
@@ -25,7 +25,8 @@ static void js_flipper_get_battery(struct mjs* mjs) {
|
|||||||
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
|
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
|
||||||
}
|
}
|
||||||
|
|
||||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object) {
|
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
|
UNUSED(modules);
|
||||||
mjs_val_t flipper_obj = mjs_mk_object(mjs);
|
mjs_val_t flipper_obj = mjs_mk_object(mjs);
|
||||||
mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
|
mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
|
||||||
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "../js_thread_i.h"
|
#include "../js_thread_i.h"
|
||||||
|
|
||||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object);
|
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||||
|
|||||||
@@ -1,387 +1,337 @@
|
|||||||
#include "../js_modules.h"
|
#include "../js_modules.h" // IWYU pragma: keep
|
||||||
|
#include "./js_event_loop/js_event_loop.h"
|
||||||
#include <furi_hal_gpio.h>
|
#include <furi_hal_gpio.h>
|
||||||
#include <furi_hal_resources.h>
|
#include <furi_hal_resources.h>
|
||||||
#include <expansion/expansion.h>
|
#include <expansion/expansion.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <mlib/m-array.h>
|
||||||
|
|
||||||
typedef struct {
|
#define INTERRUPT_QUEUE_LEN 16
|
||||||
FuriHalAdcHandle* handle;
|
|
||||||
} JsGpioInst;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-pin control structure
|
||||||
|
*/
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const GpioPin* pin;
|
const GpioPin* pin;
|
||||||
const char* name;
|
bool had_interrupt;
|
||||||
const FuriHalAdcChannel channel;
|
FuriSemaphore* interrupt_semaphore;
|
||||||
} GpioPinCtx;
|
JsEventLoopContract* interrupt_contract;
|
||||||
|
FuriHalAdcChannel adc_channel;
|
||||||
|
FuriHalAdcHandle* adc_handle;
|
||||||
|
} JsGpioPinInst;
|
||||||
|
|
||||||
static const GpioPinCtx js_gpio_pins[] = {
|
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
|
||||||
{.pin = &gpio_ext_pa7, .name = "PA7", .channel = FuriHalAdcChannel12}, // 2
|
|
||||||
{.pin = &gpio_ext_pa6, .name = "PA6", .channel = FuriHalAdcChannel11}, // 3
|
|
||||||
{.pin = &gpio_ext_pa4, .name = "PA4", .channel = FuriHalAdcChannel9}, // 4
|
|
||||||
{.pin = &gpio_ext_pb3, .name = "PB3", .channel = FuriHalAdcChannelNone}, // 5
|
|
||||||
{.pin = &gpio_ext_pb2, .name = "PB2", .channel = FuriHalAdcChannelNone}, // 6
|
|
||||||
{.pin = &gpio_ext_pc3, .name = "PC3", .channel = FuriHalAdcChannel4}, // 7
|
|
||||||
{.pin = &gpio_swclk, .name = "PA14", .channel = FuriHalAdcChannelNone}, // 10
|
|
||||||
{.pin = &gpio_swdio, .name = "PA13", .channel = FuriHalAdcChannelNone}, // 12
|
|
||||||
{.pin = &gpio_usart_tx, .name = "PB6", .channel = FuriHalAdcChannelNone}, // 13
|
|
||||||
{.pin = &gpio_usart_rx, .name = "PB7", .channel = FuriHalAdcChannelNone}, // 14
|
|
||||||
{.pin = &gpio_ext_pc1, .name = "PC1", .channel = FuriHalAdcChannel2}, // 15
|
|
||||||
{.pin = &gpio_ext_pc0, .name = "PC0", .channel = FuriHalAdcChannel1}, // 16
|
|
||||||
{.pin = &gpio_ibutton, .name = "PB14", .channel = FuriHalAdcChannelNone}, // 17
|
|
||||||
};
|
|
||||||
|
|
||||||
bool js_gpio_get_gpio_pull(const char* pull, GpioPull* value) {
|
/**
|
||||||
if(strcmp(pull, "no") == 0) {
|
* Per-module instance control structure
|
||||||
*value = GpioPullNo;
|
*/
|
||||||
return true;
|
typedef struct {
|
||||||
} else if(strcmp(pull, "up") == 0) {
|
FuriEventLoop* loop;
|
||||||
*value = GpioPullUp;
|
ManagedPinsArray_t managed_pins;
|
||||||
return true;
|
FuriHalAdcHandle* adc_handle;
|
||||||
} else if(strcmp(pull, "down") == 0) {
|
} JsGpioInst;
|
||||||
*value = GpioPullDown;
|
|
||||||
return true;
|
/**
|
||||||
} else {
|
* @brief Interrupt callback
|
||||||
*value = GpioPullNo;
|
*/
|
||||||
return true;
|
static void js_gpio_int_cb(void* arg) {
|
||||||
}
|
furi_assert(arg);
|
||||||
return false;
|
FuriSemaphore* semaphore = arg;
|
||||||
}
|
furi_semaphore_release(semaphore);
|
||||||
|
|
||||||
bool js_gpio_get_gpio_mode(const char* mode, GpioMode* value) {
|
|
||||||
if(strcmp(mode, "input") == 0) {
|
|
||||||
*value = GpioModeInput;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "outputPushPull") == 0) {
|
|
||||||
*value = GpioModeOutputPushPull;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "outputOpenDrain") == 0) {
|
|
||||||
*value = GpioModeOutputOpenDrain;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "altFunctionPushPull") == 0) {
|
|
||||||
*value = GpioModeAltFunctionPushPull;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "altFunctionOpenDrain") == 0) {
|
|
||||||
*value = GpioModeAltFunctionOpenDrain;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "analog") == 0) {
|
|
||||||
*value = GpioModeAnalog;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "interruptRise") == 0) {
|
|
||||||
*value = GpioModeInterruptRise;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "interruptFall") == 0) {
|
|
||||||
*value = GpioModeInterruptFall;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "interruptRiseFall") == 0) {
|
|
||||||
*value = GpioModeInterruptRiseFall;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "eventRise") == 0) {
|
|
||||||
*value = GpioModeEventRise;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "eventFall") == 0) {
|
|
||||||
*value = GpioModeEventFall;
|
|
||||||
return true;
|
|
||||||
} else if(strcmp(mode, "eventRiseFall") == 0) {
|
|
||||||
*value = GpioModeEventRiseFall;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const GpioPin* js_gpio_get_gpio_pin(const char* name) {
|
|
||||||
for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) {
|
|
||||||
if(strcmp(js_gpio_pins[i].name, name) == 0) {
|
|
||||||
return js_gpio_pins[i].pin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
FuriHalAdcChannel js_gpio_get_gpio_channel(const char* name) {
|
|
||||||
for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) {
|
|
||||||
if(strcmp(js_gpio_pins[i].name, name) == 0) {
|
|
||||||
return js_gpio_pins[i].channel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FuriHalAdcChannelNone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
static void js_gpio_init(struct mjs* mjs) {
|
||||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
// deconstruct mode object
|
||||||
mjs_val_t mode_arg = mjs_arg(mjs, 1);
|
mjs_val_t mode_arg;
|
||||||
mjs_val_t pull_arg = mjs_arg(mjs, 2);
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg));
|
||||||
|
mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0);
|
||||||
|
mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0);
|
||||||
|
mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0);
|
||||||
|
mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0);
|
||||||
|
mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0);
|
||||||
|
|
||||||
if(!mjs_is_string(pin_arg)) {
|
// get strings
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
const char* direction = mjs_get_string(mjs, &direction_arg, NULL);
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL);
|
||||||
return;
|
const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL);
|
||||||
|
const char* edge = mjs_get_string(mjs, &edge_arg, NULL);
|
||||||
|
const char* pull = mjs_get_string(mjs, &pull_arg, NULL);
|
||||||
|
if(!direction)
|
||||||
|
JS_ERROR_AND_RETURN(
|
||||||
|
mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object");
|
||||||
|
if(!out_mode) out_mode = "open_drain";
|
||||||
|
if(!in_mode) in_mode = "plain_digital";
|
||||||
|
if(!edge) edge = "rising";
|
||||||
|
|
||||||
|
// convert strings to mode
|
||||||
|
GpioMode mode;
|
||||||
|
if(strcmp(direction, "out") == 0) {
|
||||||
|
if(strcmp(out_mode, "push_pull") == 0)
|
||||||
|
mode = GpioModeOutputPushPull;
|
||||||
|
else if(strcmp(out_mode, "open_drain") == 0)
|
||||||
|
mode = GpioModeOutputOpenDrain;
|
||||||
|
else
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode");
|
||||||
|
} else if(strcmp(direction, "in") == 0) {
|
||||||
|
if(strcmp(in_mode, "analog") == 0) {
|
||||||
|
mode = GpioModeAnalog;
|
||||||
|
} else if(strcmp(in_mode, "plain_digital") == 0) {
|
||||||
|
mode = GpioModeInput;
|
||||||
|
} else if(strcmp(in_mode, "interrupt") == 0) {
|
||||||
|
if(strcmp(edge, "rising") == 0)
|
||||||
|
mode = GpioModeInterruptRise;
|
||||||
|
else if(strcmp(edge, "falling") == 0)
|
||||||
|
mode = GpioModeInterruptFall;
|
||||||
|
else if(strcmp(edge, "both") == 0)
|
||||||
|
mode = GpioModeInterruptRiseFall;
|
||||||
|
else
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||||
|
} else if(strcmp(in_mode, "event") == 0) {
|
||||||
|
if(strcmp(edge, "rising") == 0)
|
||||||
|
mode = GpioModeEventRise;
|
||||||
|
else if(strcmp(edge, "falling") == 0)
|
||||||
|
mode = GpioModeEventFall;
|
||||||
|
else if(strcmp(edge, "both") == 0)
|
||||||
|
mode = GpioModeEventRiseFall;
|
||||||
|
else
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction");
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
// convert pull
|
||||||
if(!pin_name) {
|
GpioPull pull_mode;
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
if(!pull) {
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
pull_mode = GpioPullNo;
|
||||||
return;
|
} else if(strcmp(pull, "up") == 0) {
|
||||||
|
pull_mode = GpioPullUp;
|
||||||
|
} else if(strcmp(pull, "down") == 0) {
|
||||||
|
pull_mode = GpioPullDown;
|
||||||
|
} else {
|
||||||
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!mjs_is_string(mode_arg)) {
|
// init GPIO
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* mode_name = mjs_get_string(mjs, &mode_arg, NULL);
|
|
||||||
if(!mode_name) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get mode name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!mjs_is_string(pull_arg)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* pull_name = mjs_get_string(mjs, &pull_arg, NULL);
|
|
||||||
if(!pull_name) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pull name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name);
|
|
||||||
if(gpio_pin == NULL) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GpioMode gpio_mode;
|
|
||||||
if(!js_gpio_get_gpio_mode(mode_name, &gpio_mode)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid mode name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GpioPull gpio_pull;
|
|
||||||
if(!js_gpio_get_gpio_pull(pull_name, &gpio_pull)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pull name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
expansion_disable(furi_record_open(RECORD_EXPANSION));
|
|
||||||
furi_record_close(RECORD_EXPANSION);
|
|
||||||
|
|
||||||
furi_hal_gpio_init(gpio_pin, gpio_mode, gpio_pull, GpioSpeedVeryHigh);
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 void js_gpio_write(struct mjs* mjs) {
|
||||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
bool level;
|
||||||
mjs_val_t value_arg = mjs_arg(mjs, 1);
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level));
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
if(!mjs_is_string(pin_arg)) {
|
furi_hal_gpio_write(manager_data->pin, level);
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
|
||||||
if(!pin_name) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!mjs_is_boolean(value_arg)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a boolean");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool value = mjs_get_bool(mjs, value_arg);
|
|
||||||
|
|
||||||
const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name);
|
|
||||||
|
|
||||||
if(gpio_pin == NULL) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
furi_hal_gpio_write(gpio_pin, value);
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
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) {
|
static void js_gpio_read(struct mjs* mjs) {
|
||||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
// get level
|
||||||
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
if(!mjs_is_string(pin_arg)) {
|
bool value = furi_hal_gpio_read(manager_data->pin);
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
|
||||||
if(!pin_name) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name);
|
|
||||||
|
|
||||||
if(gpio_pin == NULL) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool value = furi_hal_gpio_read(gpio_pin);
|
|
||||||
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, value));
|
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.read_analog(), "mV");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
static void js_gpio_read_analog(struct mjs* mjs) {
|
static void js_gpio_read_analog(struct mjs* mjs) {
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
// get mV (ADC is configured for 12 bits and 2048 mV max)
|
||||||
JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst);
|
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||||
furi_assert(gpio);
|
uint16_t millivolts =
|
||||||
|
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
|
||||||
if(gpio->handle == NULL) {
|
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
|
||||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t pin_arg = mjs_arg(mjs, 0);
|
|
||||||
|
|
||||||
if(!mjs_is_string(pin_arg)) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL);
|
|
||||||
if(!pin_name) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FuriHalAdcChannel channel = js_gpio_get_gpio_channel(pin_name);
|
|
||||||
if(channel == FuriHalAdcChannelNone) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t adc_value = furi_hal_adc_read(gpio->handle, channel);
|
|
||||||
float adc_mv = furi_hal_adc_convert_to_voltage(gpio->handle, adc_value);
|
|
||||||
|
|
||||||
mjs_return(mjs, mjs_mk_number(mjs, adc_mv));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_gpio_start_analog(struct mjs* mjs) {
|
/**
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
* @brief Returns an object that manages a specified pin.
|
||||||
JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst);
|
*
|
||||||
furi_assert(gpio);
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* let gpio = require("gpio");
|
||||||
|
* let led = gpio.get("pc3");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static void js_gpio_get(struct mjs* mjs) {
|
||||||
|
mjs_val_t name_arg;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg));
|
||||||
|
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
|
||||||
|
const GpioPinRecord* pin_record = NULL;
|
||||||
|
|
||||||
FuriHalAdcScale scale = FuriHalAdcScale2048;
|
// parse input argument to a pin pointer
|
||||||
if(mjs_nargs(mjs) > 0) {
|
if(name_string) {
|
||||||
mjs_val_t scale_arg = mjs_arg(mjs, 0);
|
pin_record = furi_hal_resources_pin_by_name(name_string);
|
||||||
|
} else if(mjs_is_number(name_arg)) {
|
||||||
if(!mjs_is_number(scale_arg)) {
|
int name_int = mjs_get_int(mjs, name_arg);
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a number");
|
pin_record = furi_hal_resources_pin_by_number(name_int);
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
} else {
|
||||||
return;
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
|
||||||
}
|
|
||||||
|
|
||||||
int32_t scale_num = mjs_get_int32(mjs, scale_arg);
|
|
||||||
if(scale_num == 2048 || scale_num == 2000) { // 2 volt reference
|
|
||||||
scale = FuriHalAdcScale2048;
|
|
||||||
} else if(scale_num == 2500) { // 2.5 volt reference
|
|
||||||
scale = FuriHalAdcScale2500;
|
|
||||||
} else {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid scale");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(gpio->handle != NULL) {
|
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
|
||||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode already started");
|
if(pin_record->debug)
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gpio->handle = furi_hal_adc_acquire();
|
// return pin manager object
|
||||||
furi_hal_adc_configure_ex(
|
JsGpioInst* module = JS_GET_CONTEXT(mjs);
|
||||||
gpio->handle,
|
mjs_val_t manager = mjs_mk_object(mjs);
|
||||||
scale,
|
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
|
||||||
FuriHalAdcClockSync64,
|
manager_data->pin = pin_record->pin;
|
||||||
FuriHalAdcOversample64,
|
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
|
||||||
FuriHalAdcSamplingtime247_5);
|
manager_data->adc_handle = module->adc_handle;
|
||||||
|
manager_data->adc_channel = pin_record->channel;
|
||||||
|
mjs_own(mjs, &manager);
|
||||||
|
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data));
|
||||||
|
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init));
|
||||||
|
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write));
|
||||||
|
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read));
|
||||||
|
mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||||
|
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
|
||||||
|
mjs_return(mjs, manager);
|
||||||
|
|
||||||
|
// remember pin
|
||||||
|
ManagedPinsArray_push_back(module->managed_pins, manager_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_gpio_stop_analog(struct mjs* mjs) {
|
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||||
JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst);
|
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||||
furi_assert(gpio);
|
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||||
|
|
||||||
if(gpio->handle == NULL) {
|
JsGpioInst* module = malloc(sizeof(JsGpioInst));
|
||||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started");
|
ManagedPinsArray_init(module->managed_pins);
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
module->adc_handle = furi_hal_adc_acquire();
|
||||||
return;
|
module->loop = loop;
|
||||||
}
|
furi_hal_adc_configure(module->adc_handle);
|
||||||
|
|
||||||
furi_hal_adc_release(gpio->handle);
|
|
||||||
gpio->handle = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object) {
|
|
||||||
JsGpioInst* gpio = malloc(sizeof(JsGpioInst));
|
|
||||||
gpio->handle = NULL;
|
|
||||||
mjs_val_t gpio_obj = mjs_mk_object(mjs);
|
mjs_val_t gpio_obj = mjs_mk_object(mjs);
|
||||||
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, gpio));
|
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||||
mjs_set(mjs, gpio_obj, "init", ~0, MJS_MK_FN(js_gpio_init));
|
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
|
||||||
mjs_set(mjs, gpio_obj, "write", ~0, MJS_MK_FN(js_gpio_write));
|
|
||||||
mjs_set(mjs, gpio_obj, "read", ~0, MJS_MK_FN(js_gpio_read));
|
|
||||||
mjs_set(mjs, gpio_obj, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
|
||||||
mjs_set(mjs, gpio_obj, "startAnalog", ~0, MJS_MK_FN(js_gpio_start_analog));
|
|
||||||
mjs_set(mjs, gpio_obj, "stopAnalog", ~0, MJS_MK_FN(js_gpio_stop_analog));
|
|
||||||
*object = gpio_obj;
|
*object = gpio_obj;
|
||||||
|
|
||||||
return (void*)gpio;
|
return (void*)module;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_gpio_destroy(void* inst) {
|
static void js_gpio_destroy(void* inst) {
|
||||||
if(inst != NULL) {
|
furi_assert(inst);
|
||||||
JsGpioInst* gpio = (JsGpioInst*)inst;
|
JsGpioInst* module = (JsGpioInst*)inst;
|
||||||
if(gpio->handle != NULL) {
|
|
||||||
furi_hal_adc_release(gpio->handle);
|
// reset pins
|
||||||
gpio->handle = NULL;
|
ManagedPinsArray_it_t iterator;
|
||||||
|
for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator);
|
||||||
|
ManagedPinsArray_next(iterator)) {
|
||||||
|
JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator);
|
||||||
|
if(manager_data->had_interrupt) {
|
||||||
|
furi_hal_gpio_disable_int_callback(manager_data->pin);
|
||||||
|
furi_hal_gpio_remove_int_callback(manager_data->pin);
|
||||||
}
|
}
|
||||||
free(gpio);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop through all pins and reset them to analog mode
|
// free buffers
|
||||||
for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) {
|
furi_hal_adc_release(module->adc_handle);
|
||||||
furi_hal_gpio_write(js_gpio_pins[i].pin, false);
|
ManagedPinsArray_clear(module->managed_pins);
|
||||||
furi_hal_gpio_init(js_gpio_pins[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh);
|
free(module);
|
||||||
}
|
|
||||||
|
|
||||||
expansion_enable(furi_record_open(RECORD_EXPANSION));
|
|
||||||
furi_record_close(RECORD_EXPANSION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const JsModuleDescriptor js_gpio_desc = {
|
static const JsModuleDescriptor js_gpio_desc = {
|
||||||
"gpio",
|
"gpio",
|
||||||
js_gpio_create,
|
js_gpio_create,
|
||||||
js_gpio_destroy,
|
js_gpio_destroy,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#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);
|
||||||
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#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);
|
||||||
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
||||||
|
int32_t event;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&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) {
|
||||||
|
enum {
|
||||||
|
SendDirToFront,
|
||||||
|
SendDirToBack,
|
||||||
|
} send_direction;
|
||||||
|
JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack});
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection"));
|
||||||
|
|
||||||
|
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||||
|
if(send_direction == SendDirToBack) {
|
||||||
|
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) {
|
||||||
|
mjs_val_t view;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view));
|
||||||
|
JsGuiViewData* view_data = JS_GET_INST(mjs, view);
|
||||||
|
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||||
|
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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){.array = 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 `View.set`
|
||||||
|
*/
|
||||||
|
static void js_gui_view_set(struct mjs* mjs) {
|
||||||
|
const char* name;
|
||||||
|
mjs_val_t value;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&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` 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);
|
||||||
|
mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set));
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
|
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) {
|
||||||
|
mjs_val_t props;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#include "../../js_modules.h"
|
||||||
|
#include <gui/view.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JsViewPropTypeString,
|
||||||
|
JsViewPropTypeNumber,
|
||||||
|
JsViewPropTypeArr,
|
||||||
|
} JsViewPropType;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
const char* string;
|
||||||
|
int32_t number;
|
||||||
|
mjs_val_t array;
|
||||||
|
} JsViewPropValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 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
|
||||||
|
size_t prop_cnt; //<! Number of properties visible from JS
|
||||||
|
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
|
||||||
|
} JsViewDescriptor;
|
||||||
|
|
||||||
|
// Callback ordering:
|
||||||
|
// alloc -> get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free
|
||||||
|
// \_______________ creation ________________/ \___ usage ___/ \_________ 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
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#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(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#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*))));
|
||||||
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#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);
|
||||||
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#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 {
|
||||||
|
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 items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||||
|
UNUSED(mjs);
|
||||||
|
submenu_reset(submenu);
|
||||||
|
size_t len = mjs_array_length(mjs, value.array);
|
||||||
|
for(size_t i = 0; i < len; i++) {
|
||||||
|
mjs_val_t item = mjs_array_get(mjs, value.array, i);
|
||||||
|
if(!mjs_is_string(item)) return false;
|
||||||
|
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
.prop_cnt = 2,
|
||||||
|
.props = {
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "header",
|
||||||
|
.type = JsViewPropTypeString,
|
||||||
|
.assign = (JsViewPropAssign)header_assign},
|
||||||
|
(JsViewPropDescriptor){
|
||||||
|
.name = "items",
|
||||||
|
.type = JsViewPropTypeArr,
|
||||||
|
.assign = (JsViewPropAssign)items_assign},
|
||||||
|
}};
|
||||||
|
JS_GUI_VIEW_DEF(submenu, &view_descriptor);
|
||||||
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#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);
|
||||||
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#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;
|
||||||
|
FuriString* header;
|
||||||
|
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);
|
||||||
|
context->buffer_size = (size_t)(value.number + 1);
|
||||||
|
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
|
||||||
|
text_input_set_result_callback(
|
||||||
|
input,
|
||||||
|
(TextInputCallback)input_callback,
|
||||||
|
context,
|
||||||
|
context->buffer,
|
||||||
|
context->buffer_size,
|
||||||
|
true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
|
||||||
|
UNUSED(input);
|
||||||
|
JsKbdContext* context = malloc(sizeof(JsKbdContext));
|
||||||
|
*context = (JsKbdContext){
|
||||||
|
.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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
UNUSED(mjs);
|
||||||
|
UNUSED(view_obj);
|
||||||
|
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 = 3,
|
||||||
|
.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},
|
||||||
|
}};
|
||||||
|
|
||||||
|
JS_GUI_VIEW_DEF(text_input, &view_descriptor);
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
#include "../js_modules.h"
|
|
||||||
#include <gui/modules/text_input.h>
|
|
||||||
#include <gui/modules/byte_input.h>
|
|
||||||
#include <gui/view_holder.h>
|
|
||||||
#include <toolbox/api_lock.h>
|
|
||||||
|
|
||||||
#define membersof(x) (sizeof(x) / sizeof(x[0]))
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
TextInput* text_input;
|
|
||||||
ByteInput* byte_input;
|
|
||||||
ViewHolder* view_holder;
|
|
||||||
FuriApiLock lock;
|
|
||||||
char* header;
|
|
||||||
bool accepted;
|
|
||||||
} JsKeyboardInst;
|
|
||||||
|
|
||||||
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 JsKeyboardInst* get_this_ctx(struct mjs* mjs) {
|
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
|
||||||
JsKeyboardInst* keyboard = mjs_get_ptr(mjs, obj_inst);
|
|
||||||
furi_assert(keyboard);
|
|
||||||
return keyboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void keyboard_callback(void* context) {
|
|
||||||
JsKeyboardInst* keyboard = (JsKeyboardInst*)context;
|
|
||||||
keyboard->accepted = true;
|
|
||||||
api_lock_unlock(keyboard->lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void keyboard_exit(void* context) {
|
|
||||||
JsKeyboardInst* keyboard = (JsKeyboardInst*)context;
|
|
||||||
keyboard->accepted = false;
|
|
||||||
api_lock_unlock(keyboard->lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_keyboard_set_header(struct mjs* mjs) {
|
|
||||||
JsKeyboardInst* keyboard = get_this_ctx(mjs);
|
|
||||||
|
|
||||||
mjs_val_t header_arg = mjs_arg(mjs, 0);
|
|
||||||
const char* header = mjs_get_string(mjs, &header_arg, NULL);
|
|
||||||
if(!header) {
|
|
||||||
ret_bad_args(mjs, "Header must be a string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(keyboard->header) {
|
|
||||||
free(keyboard->header);
|
|
||||||
}
|
|
||||||
keyboard->header = strdup(header);
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_keyboard_text(struct mjs* mjs) {
|
|
||||||
JsKeyboardInst* keyboard = get_this_ctx(mjs);
|
|
||||||
|
|
||||||
mjs_val_t input_length_arg = mjs_arg(mjs, 0);
|
|
||||||
if(!mjs_is_number(input_length_arg)) {
|
|
||||||
ret_bad_args(mjs, "Input length must be a number");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int32_t input_length = mjs_get_int32(mjs, input_length_arg);
|
|
||||||
char* buffer = malloc(input_length);
|
|
||||||
|
|
||||||
mjs_val_t default_text_arg = mjs_arg(mjs, 1);
|
|
||||||
const char* default_text = mjs_get_string(mjs, &default_text_arg, NULL);
|
|
||||||
bool clear_default = false;
|
|
||||||
if(default_text) {
|
|
||||||
strlcpy(buffer, default_text, input_length);
|
|
||||||
mjs_val_t bool_obj = mjs_arg(mjs, 2);
|
|
||||||
clear_default = mjs_get_bool(mjs, bool_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(keyboard->header) {
|
|
||||||
text_input_set_header_text(keyboard->text_input, keyboard->header);
|
|
||||||
}
|
|
||||||
text_input_set_result_callback(
|
|
||||||
keyboard->text_input, keyboard_callback, keyboard, buffer, input_length, clear_default);
|
|
||||||
|
|
||||||
text_input_set_minimum_length(keyboard->text_input, 0);
|
|
||||||
|
|
||||||
keyboard->lock = api_lock_alloc_locked();
|
|
||||||
Gui* gui = furi_record_open(RECORD_GUI);
|
|
||||||
keyboard->view_holder = view_holder_alloc();
|
|
||||||
view_holder_attach_to_gui(keyboard->view_holder, gui);
|
|
||||||
view_holder_set_back_callback(keyboard->view_holder, keyboard_exit, keyboard);
|
|
||||||
|
|
||||||
view_holder_set_view(keyboard->view_holder, text_input_get_view(keyboard->text_input));
|
|
||||||
api_lock_wait_unlock(keyboard->lock);
|
|
||||||
|
|
||||||
view_holder_set_view(keyboard->view_holder, NULL);
|
|
||||||
view_holder_free(keyboard->view_holder);
|
|
||||||
|
|
||||||
furi_record_close(RECORD_GUI);
|
|
||||||
api_lock_free(keyboard->lock);
|
|
||||||
|
|
||||||
text_input_reset(keyboard->text_input);
|
|
||||||
if(keyboard->header) {
|
|
||||||
free(keyboard->header);
|
|
||||||
keyboard->header = NULL;
|
|
||||||
}
|
|
||||||
if(keyboard->accepted) {
|
|
||||||
mjs_return(mjs, mjs_mk_string(mjs, buffer, ~0, true));
|
|
||||||
} else {
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
free(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_keyboard_byte(struct mjs* mjs) {
|
|
||||||
JsKeyboardInst* keyboard = get_this_ctx(mjs);
|
|
||||||
|
|
||||||
mjs_val_t input_length_arg = mjs_arg(mjs, 0);
|
|
||||||
if(!mjs_is_number(input_length_arg)) {
|
|
||||||
ret_bad_args(mjs, "Input length must be a number");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int32_t input_length = mjs_get_int32(mjs, input_length_arg);
|
|
||||||
uint8_t* buffer = malloc(input_length);
|
|
||||||
|
|
||||||
mjs_val_t default_data_arg = mjs_arg(mjs, 1);
|
|
||||||
if(mjs_is_typed_array(default_data_arg)) {
|
|
||||||
if(mjs_is_data_view(default_data_arg)) {
|
|
||||||
default_data_arg = mjs_dataview_get_buf(mjs, default_data_arg);
|
|
||||||
}
|
|
||||||
size_t default_data_len = 0;
|
|
||||||
char* default_data = mjs_array_buf_get_ptr(mjs, default_data_arg, &default_data_len);
|
|
||||||
memcpy(buffer, (uint8_t*)default_data, MIN((size_t)input_length, default_data_len));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(keyboard->header) {
|
|
||||||
byte_input_set_header_text(keyboard->byte_input, keyboard->header);
|
|
||||||
}
|
|
||||||
byte_input_set_result_callback(
|
|
||||||
keyboard->byte_input, keyboard_callback, NULL, keyboard, buffer, input_length);
|
|
||||||
|
|
||||||
keyboard->lock = api_lock_alloc_locked();
|
|
||||||
Gui* gui = furi_record_open(RECORD_GUI);
|
|
||||||
keyboard->view_holder = view_holder_alloc();
|
|
||||||
view_holder_attach_to_gui(keyboard->view_holder, gui);
|
|
||||||
view_holder_set_back_callback(keyboard->view_holder, keyboard_exit, keyboard);
|
|
||||||
|
|
||||||
view_holder_set_view(keyboard->view_holder, byte_input_get_view(keyboard->byte_input));
|
|
||||||
api_lock_wait_unlock(keyboard->lock);
|
|
||||||
|
|
||||||
view_holder_set_view(keyboard->view_holder, NULL);
|
|
||||||
view_holder_free(keyboard->view_holder);
|
|
||||||
|
|
||||||
furi_record_close(RECORD_GUI);
|
|
||||||
api_lock_free(keyboard->lock);
|
|
||||||
|
|
||||||
if(keyboard->header) {
|
|
||||||
free(keyboard->header);
|
|
||||||
keyboard->header = NULL;
|
|
||||||
}
|
|
||||||
byte_input_set_result_callback(keyboard->byte_input, NULL, NULL, NULL, NULL, 0);
|
|
||||||
byte_input_set_header_text(keyboard->byte_input, "");
|
|
||||||
if(keyboard->accepted) {
|
|
||||||
mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)buffer, input_length));
|
|
||||||
} else {
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
free(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void* js_keyboard_create(struct mjs* mjs, mjs_val_t* object) {
|
|
||||||
JsKeyboardInst* keyboard = malloc(sizeof(JsKeyboardInst));
|
|
||||||
mjs_val_t keyboard_obj = mjs_mk_object(mjs);
|
|
||||||
mjs_set(mjs, keyboard_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, keyboard));
|
|
||||||
mjs_set(mjs, keyboard_obj, "setHeader", ~0, MJS_MK_FN(js_keyboard_set_header));
|
|
||||||
mjs_set(mjs, keyboard_obj, "text", ~0, MJS_MK_FN(js_keyboard_text));
|
|
||||||
mjs_set(mjs, keyboard_obj, "byte", ~0, MJS_MK_FN(js_keyboard_byte));
|
|
||||||
keyboard->byte_input = byte_input_alloc();
|
|
||||||
keyboard->text_input = text_input_alloc();
|
|
||||||
*object = keyboard_obj;
|
|
||||||
return keyboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_keyboard_destroy(void* inst) {
|
|
||||||
JsKeyboardInst* keyboard = inst;
|
|
||||||
byte_input_free(keyboard->byte_input);
|
|
||||||
text_input_free(keyboard->text_input);
|
|
||||||
free(keyboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const JsModuleDescriptor js_keyboard_desc = {
|
|
||||||
"keyboard",
|
|
||||||
js_keyboard_create,
|
|
||||||
js_keyboard_destroy,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
|
||||||
.appid = PLUGIN_APP_ID,
|
|
||||||
.ep_api_version = PLUGIN_API_VERSION,
|
|
||||||
.entry_point = &js_keyboard_desc,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlipperAppPluginDescriptor* js_keyboard_ep(void) {
|
|
||||||
return &plugin_descriptor;
|
|
||||||
}
|
|
||||||
@@ -305,7 +305,8 @@ void js_math_trunc(struct mjs* mjs) {
|
|||||||
mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x)));
|
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) {
|
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_val_t math_obj = mjs_mk_object(mjs);
|
||||||
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal));
|
mjs_set(mjs, math_obj, "is_equal", ~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, "abs", ~0, MJS_MK_FN(js_math_abs));
|
||||||
@@ -342,6 +343,7 @@ static const JsModuleDescriptor js_math_desc = {
|
|||||||
"math",
|
"math",
|
||||||
js_math_create,
|
js_math_create,
|
||||||
NULL,
|
NULL,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ static void js_notify_blink(struct mjs* mjs) {
|
|||||||
mjs_return(mjs, MJS_UNDEFINED);
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object) {
|
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
|
UNUSED(modules);
|
||||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||||
mjs_val_t notify_obj = mjs_mk_object(mjs);
|
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, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, notification));
|
||||||
@@ -96,6 +97,7 @@ static const JsModuleDescriptor js_notification_desc = {
|
|||||||
"notification",
|
"notification",
|
||||||
js_notification_create,
|
js_notification_create,
|
||||||
js_notification_destroy,
|
js_notification_destroy,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
@@ -658,7 +658,8 @@ static void js_serial_expect(struct mjs* mjs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object) {
|
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||||
|
UNUSED(modules);
|
||||||
JsSerialInst* js_serial = malloc(sizeof(JsSerialInst));
|
JsSerialInst* js_serial = malloc(sizeof(JsSerialInst));
|
||||||
js_serial->mjs = mjs;
|
js_serial->mjs = mjs;
|
||||||
mjs_val_t serial_obj = mjs_mk_object(mjs);
|
mjs_val_t serial_obj = mjs_mk_object(mjs);
|
||||||
@@ -686,6 +687,7 @@ static const JsModuleDescriptor js_serial_desc = {
|
|||||||
"serial",
|
"serial",
|
||||||
js_serial_create,
|
js_serial_create,
|
||||||
js_serial_destroy,
|
js_serial_destroy,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
@@ -1,262 +1,375 @@
|
|||||||
#include "../js_modules.h"
|
#include "../js_modules.h" // IWYU pragma: keep
|
||||||
#include <storage/storage.h>
|
#include <path.h>
|
||||||
|
|
||||||
typedef struct {
|
// ---=== file ops ===---
|
||||||
Storage* api;
|
|
||||||
} JsStorageInst;
|
|
||||||
|
|
||||||
static JsStorageInst* get_this_ctx(struct mjs* mjs) {
|
static void js_storage_file_close(struct mjs* mjs) {
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
JsStorageInst* storage = mjs_get_ptr(mjs, obj_inst);
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
furi_assert(storage);
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file)));
|
||||||
return storage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
static void js_storage_file_is_open(struct mjs* mjs) {
|
||||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file)));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ret_int_err(struct mjs* mjs, const char* error) {
|
static void js_storage_file_read(struct mjs* mjs) {
|
||||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", error);
|
enum {
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
ReadModeAscii,
|
||||||
}
|
ReadModeBinary,
|
||||||
|
} read_mode;
|
||||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary});
|
||||||
size_t num_args = mjs_nargs(mjs);
|
int32_t length;
|
||||||
if(num_args != count) {
|
JS_FETCH_ARGS_OR_RETURN(
|
||||||
ret_bad_args(mjs, "Wrong argument count");
|
mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length));
|
||||||
return false;
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
|
char buffer[length];
|
||||||
|
size_t actually_read = storage_file_read(file, buffer, length);
|
||||||
|
if(read_mode == ReadModeAscii) {
|
||||||
|
mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true));
|
||||||
|
} else if(read_mode == ReadModeBinary) {
|
||||||
|
mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read));
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool get_path_arg(struct mjs* mjs, const char** path, size_t index) {
|
static void js_storage_file_write(struct mjs* mjs) {
|
||||||
mjs_val_t path_obj = mjs_arg(mjs, index);
|
mjs_val_t data;
|
||||||
if(!mjs_is_string(path_obj)) {
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data));
|
||||||
ret_bad_args(mjs, "Path must be a string");
|
const void* buf;
|
||||||
return false;
|
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");
|
||||||
}
|
}
|
||||||
size_t path_len = 0;
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
*path = mjs_get_string(mjs, &path_obj, &path_len);
|
mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len)));
|
||||||
if((path_len == 0) || (*path == NULL)) {
|
|
||||||
ret_bad_args(mjs, "Bad path argument");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_storage_read(struct mjs* mjs) {
|
static void js_storage_file_seek_relative(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
int32_t offset;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||||
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false)));
|
||||||
|
}
|
||||||
|
|
||||||
const char* path;
|
static void js_storage_file_seek_absolute(struct mjs* mjs) {
|
||||||
if(!get_path_arg(mjs, &path, 0)) return;
|
int32_t offset;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||||
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true)));
|
||||||
|
}
|
||||||
|
|
||||||
File* file = storage_file_alloc(storage->api);
|
static void js_storage_file_tell(struct mjs* mjs) {
|
||||||
do {
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
ret_int_err(mjs, storage_file_get_error_desc(file));
|
mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file)));
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t size = storage_file_size(file);
|
static void js_storage_file_truncate(struct mjs* mjs) {
|
||||||
mjs_val_t size_arg = mjs_arg(mjs, 1);
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
if(mjs_is_number(size_arg)) {
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
size = mjs_get_int32(mjs, size_arg);
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file)));
|
||||||
}
|
}
|
||||||
|
|
||||||
mjs_val_t seek_arg = mjs_arg(mjs, 2);
|
static void js_storage_file_size(struct mjs* mjs) {
|
||||||
if(mjs_is_number(seek_arg)) {
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true);
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
size = MIN(size, storage_file_size(file) - storage_file_tell(file));
|
mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(size > memmgr_heap_get_max_free_block()) {
|
static void js_storage_file_eof(struct mjs* mjs) {
|
||||||
ret_int_err(mjs, "Read size too large");
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||||
break;
|
File* file = JS_GET_CONTEXT(mjs);
|
||||||
}
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file)));
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t* data = malloc(size);
|
static void js_storage_file_copy_to(struct mjs* mjs) {
|
||||||
size_t read = storage_file_read(file, data, size);
|
File* source = JS_GET_CONTEXT(mjs);
|
||||||
if(read == size) {
|
mjs_val_t dest_obj;
|
||||||
mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)data, size));
|
int32_t bytes;
|
||||||
} else {
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes));
|
||||||
ret_int_err(mjs, "File read failed");
|
File* destination = JS_GET_INST(mjs, dest_obj);
|
||||||
}
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes)));
|
||||||
free(data);
|
}
|
||||||
} while(0);
|
|
||||||
|
// ---=== top-level file ops ===---
|
||||||
|
|
||||||
|
// 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);
|
storage_file_free(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_storage_write(struct mjs* mjs) {
|
static void js_storage_open_file(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
|
||||||
|
|
||||||
const char* path;
|
const char* path;
|
||||||
if(!get_path_arg(mjs, &path, 0)) return;
|
FS_AccessMode access_mode;
|
||||||
|
FS_OpenMode open_mode;
|
||||||
|
JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE});
|
||||||
|
JS_ENUM_MAP(
|
||||||
|
open_mode,
|
||||||
|
{"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});
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(
|
||||||
|
mjs,
|
||||||
|
JS_EXACTLY,
|
||||||
|
JS_ARG_STR(&path),
|
||||||
|
JS_ARG_ENUM(access_mode, "AccessMode"),
|
||||||
|
JS_ARG_ENUM(open_mode, "OpenMode"));
|
||||||
|
|
||||||
mjs_val_t data_arg = mjs_arg(mjs, 1);
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) {
|
File* file = storage_file_alloc(storage);
|
||||||
ret_bad_args(mjs, "Data must be string, arraybuf or dataview");
|
if(!storage_file_open(file, path, access_mode, open_mode)) {
|
||||||
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(mjs_is_data_view(data_arg)) {
|
|
||||||
data_arg = mjs_dataview_get_buf(mjs, data_arg);
|
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));
|
||||||
}
|
}
|
||||||
size_t data_len = 0;
|
mjs_return(mjs, file_obj);
|
||||||
const char* data = NULL;
|
|
||||||
if(mjs_is_string(data_arg)) {
|
|
||||||
data = mjs_get_string(mjs, &data_arg, &data_len);
|
|
||||||
} else if(mjs_is_typed_array(data_arg)) {
|
|
||||||
data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t seek_arg = mjs_arg(mjs, 2);
|
|
||||||
|
|
||||||
File* file = storage_file_alloc(storage->api);
|
|
||||||
if(!storage_file_open(
|
|
||||||
file,
|
|
||||||
path,
|
|
||||||
FSAM_WRITE,
|
|
||||||
mjs_is_number(seek_arg) ? FSOM_OPEN_ALWAYS : FSOM_CREATE_ALWAYS)) {
|
|
||||||
ret_int_err(mjs, storage_file_get_error_desc(file));
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if(mjs_is_number(seek_arg)) {
|
|
||||||
storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t write = storage_file_write(file, data, data_len);
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len));
|
|
||||||
}
|
|
||||||
storage_file_free(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_storage_append(struct mjs* mjs) {
|
static void js_storage_file_exists(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 2)) return;
|
|
||||||
|
|
||||||
const char* path;
|
const char* path;
|
||||||
if(!get_path_arg(mjs, &path, 0)) return;
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||||
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path)));
|
||||||
|
}
|
||||||
|
|
||||||
mjs_val_t data_arg = mjs_arg(mjs, 1);
|
// ---=== dir ops ===---
|
||||||
if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) {
|
|
||||||
ret_bad_args(mjs, "Data must be string, arraybuf or dataview");
|
static void js_storage_read_directory(struct mjs* mjs) {
|
||||||
|
const char* path;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&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;
|
return;
|
||||||
}
|
}
|
||||||
if(mjs_is_data_view(data_arg)) {
|
|
||||||
data_arg = mjs_dataview_get_buf(mjs, data_arg);
|
FileInfo file_info;
|
||||||
}
|
char name[128];
|
||||||
size_t data_len = 0;
|
FuriString* file_path = furi_string_alloc_set_str(path);
|
||||||
const char* data = NULL;
|
size_t path_size = furi_string_size(file_path);
|
||||||
if(mjs_is_string(data_arg)) {
|
uint32_t timestamp;
|
||||||
data = mjs_get_string(mjs, &data_arg, &data_len);
|
|
||||||
} else if(mjs_is_typed_array(data_arg)) {
|
mjs_val_t ret = mjs_mk_array(mjs);
|
||||||
data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len);
|
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), ×tamp) ==
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
File* file = storage_file_alloc(storage->api);
|
storage_file_free(dir);
|
||||||
if(!storage_file_open(file, path, FSAM_WRITE, FSOM_OPEN_APPEND)) {
|
furi_string_free(file_path);
|
||||||
ret_int_err(mjs, storage_file_get_error_desc(file));
|
mjs_return(mjs, ret);
|
||||||
} else {
|
|
||||||
size_t write = storage_file_write(file, data, data_len);
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len));
|
|
||||||
}
|
|
||||||
storage_file_free(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_storage_exists(struct mjs* mjs) {
|
static void js_storage_directory_exists(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 1)) return;
|
|
||||||
|
|
||||||
const char* path;
|
const char* path;
|
||||||
if(!get_path_arg(mjs, &path, 0)) return;
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||||
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path)));
|
||||||
|
}
|
||||||
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage->api, path)));
|
static void js_storage_make_directory(struct mjs* mjs) {
|
||||||
|
const char* path;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||||
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---=== common ops ===---
|
||||||
|
|
||||||
|
static void js_storage_file_or_dir_exists(struct mjs* mjs) {
|
||||||
|
const char* path;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&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_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&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, ×tamp)) != 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) {
|
static void js_storage_remove(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 1)) return;
|
|
||||||
|
|
||||||
const char* path;
|
const char* path;
|
||||||
if(!get_path_arg(mjs, &path, 0)) return;
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||||
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path)));
|
||||||
|
}
|
||||||
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage->api, path)));
|
static void js_storage_rmrf(struct mjs* mjs) {
|
||||||
|
const char* path;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&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_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&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) {
|
static void js_storage_copy(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
const char *source, *dest;
|
||||||
if(!check_arg_count(mjs, 2)) return;
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&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));
|
||||||
|
}
|
||||||
|
|
||||||
const char* old_path;
|
static void js_storage_fs_info(struct mjs* mjs) {
|
||||||
if(!get_path_arg(mjs, &old_path, 0)) return;
|
const char* fs;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs));
|
||||||
const char* new_path;
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
if(!get_path_arg(mjs, &new_path, 1)) return;
|
uint64_t total_space, free_space;
|
||||||
|
if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) {
|
||||||
FS_Error error = storage_common_copy(storage->api, old_path, new_path);
|
|
||||||
if(error == FSE_OK) {
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
mjs_return(mjs, MJS_UNDEFINED);
|
||||||
} else {
|
return;
|
||||||
ret_int_err(mjs, storage_error_get_desc(error));
|
|
||||||
}
|
}
|
||||||
}
|
mjs_val_t ret = mjs_mk_object(mjs);
|
||||||
|
JS_ASSIGN_MULTI(mjs, ret) {
|
||||||
static void js_storage_move(struct mjs* mjs) {
|
JS_FIELD("totalSpace", mjs_mk_number(mjs, total_space));
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
JS_FIELD("freeSpace", mjs_mk_number(mjs, free_space));
|
||||||
if(!check_arg_count(mjs, 2)) return;
|
|
||||||
|
|
||||||
const char* old_path;
|
|
||||||
if(!get_path_arg(mjs, &old_path, 0)) return;
|
|
||||||
|
|
||||||
const char* new_path;
|
|
||||||
if(!get_path_arg(mjs, &new_path, 1)) return;
|
|
||||||
|
|
||||||
FS_Error error = storage_common_rename(storage->api, old_path, new_path);
|
|
||||||
if(error == FSE_OK) {
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
} else {
|
|
||||||
ret_int_err(mjs, storage_error_get_desc(error));
|
|
||||||
}
|
}
|
||||||
|
mjs_return(mjs, ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_storage_mkdir(struct mjs* mjs) {
|
static void js_storage_next_available_filename(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = get_this_ctx(mjs);
|
const char *dir_path, *file_name, *file_ext;
|
||||||
if(!check_arg_count(mjs, 1)) return;
|
int32_t max_len;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(
|
||||||
const char* path;
|
mjs,
|
||||||
if(!get_path_arg(mjs, &path, 0)) return;
|
JS_EXACTLY,
|
||||||
|
JS_ARG_STR(&dir_path),
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage->api, path)));
|
JS_ARG_STR(&file_name),
|
||||||
|
JS_ARG_STR(&file_ext),
|
||||||
|
JS_ARG_INT32(&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);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void* js_storage_create(struct mjs* mjs, mjs_val_t* object) {
|
// ---=== path ops ===---
|
||||||
JsStorageInst* storage = malloc(sizeof(JsStorageInst));
|
|
||||||
mjs_val_t storage_obj = mjs_mk_object(mjs);
|
static void js_storage_are_paths_equal(struct mjs* mjs) {
|
||||||
mjs_set(mjs, storage_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, storage));
|
const char *path1, *path2;
|
||||||
mjs_set(mjs, storage_obj, "read", ~0, MJS_MK_FN(js_storage_read));
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2));
|
||||||
mjs_set(mjs, storage_obj, "write", ~0, MJS_MK_FN(js_storage_write));
|
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||||
mjs_set(mjs, storage_obj, "append", ~0, MJS_MK_FN(js_storage_append));
|
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2)));
|
||||||
mjs_set(mjs, storage_obj, "exists", ~0, MJS_MK_FN(js_storage_exists));
|
|
||||||
mjs_set(mjs, storage_obj, "remove", ~0, MJS_MK_FN(js_storage_remove));
|
|
||||||
mjs_set(mjs, storage_obj, "copy", ~0, MJS_MK_FN(js_storage_copy));
|
|
||||||
mjs_set(mjs, storage_obj, "move", ~0, MJS_MK_FN(js_storage_move));
|
|
||||||
mjs_set(mjs, storage_obj, "mkdir", ~0, MJS_MK_FN(js_storage_mkdir));
|
|
||||||
storage->api = furi_record_open(RECORD_STORAGE);
|
|
||||||
*object = storage_obj;
|
|
||||||
return storage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void js_storage_destroy(void* inst) {
|
static void js_storage_is_subpath_of(struct mjs* mjs) {
|
||||||
JsStorageInst* storage = inst;
|
const char *parent, *child;
|
||||||
|
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&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);
|
furi_record_close(RECORD_STORAGE);
|
||||||
free(storage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---=== boilerplate ===---
|
||||||
|
|
||||||
static const JsModuleDescriptor js_storage_desc = {
|
static const JsModuleDescriptor js_storage_desc = {
|
||||||
"storage",
|
"storage",
|
||||||
js_storage_create,
|
js_storage_create,
|
||||||
js_storage_destroy,
|
js_storage_destroy,
|
||||||
|
NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
#include <gui/modules/submenu.h>
|
|
||||||
#include <gui/view_holder.h>
|
|
||||||
#include <gui/view.h>
|
|
||||||
#include <toolbox/api_lock.h>
|
|
||||||
#include "../js_modules.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
Submenu* submenu;
|
|
||||||
ViewHolder* view_holder;
|
|
||||||
FuriApiLock lock;
|
|
||||||
uint32_t result;
|
|
||||||
bool accepted;
|
|
||||||
} JsSubmenuInst;
|
|
||||||
|
|
||||||
static JsSubmenuInst* get_this_ctx(struct mjs* mjs) {
|
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
|
||||||
JsSubmenuInst* submenu = mjs_get_ptr(mjs, obj_inst);
|
|
||||||
furi_assert(submenu);
|
|
||||||
return submenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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 submenu_callback(void* context, uint32_t id) {
|
|
||||||
JsSubmenuInst* submenu = context;
|
|
||||||
submenu->result = id;
|
|
||||||
submenu->accepted = true;
|
|
||||||
api_lock_unlock(submenu->lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void submenu_exit(void* context) {
|
|
||||||
JsSubmenuInst* submenu = context;
|
|
||||||
submenu->result = 0;
|
|
||||||
submenu->accepted = false;
|
|
||||||
api_lock_unlock(submenu->lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_submenu_add_item(struct mjs* mjs) {
|
|
||||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 2)) return;
|
|
||||||
|
|
||||||
mjs_val_t label_arg = mjs_arg(mjs, 0);
|
|
||||||
const char* label = mjs_get_string(mjs, &label_arg, NULL);
|
|
||||||
if(!label) {
|
|
||||||
ret_bad_args(mjs, "Label must be a string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjs_val_t id_arg = mjs_arg(mjs, 1);
|
|
||||||
if(!mjs_is_number(id_arg)) {
|
|
||||||
ret_bad_args(mjs, "Id must be a number");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int32_t id = mjs_get_int32(mjs, id_arg);
|
|
||||||
|
|
||||||
submenu_add_item(submenu->submenu, label, id, submenu_callback, submenu);
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_submenu_set_header(struct mjs* mjs) {
|
|
||||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 1)) return;
|
|
||||||
|
|
||||||
mjs_val_t header_arg = mjs_arg(mjs, 0);
|
|
||||||
const char* header = mjs_get_string(mjs, &header_arg, NULL);
|
|
||||||
if(!header) {
|
|
||||||
ret_bad_args(mjs, "Header must be a string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
submenu_set_header(submenu->submenu, header);
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_submenu_show(struct mjs* mjs) {
|
|
||||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 0)) return;
|
|
||||||
|
|
||||||
submenu->lock = api_lock_alloc_locked();
|
|
||||||
Gui* gui = furi_record_open(RECORD_GUI);
|
|
||||||
submenu->view_holder = view_holder_alloc();
|
|
||||||
view_holder_attach_to_gui(submenu->view_holder, gui);
|
|
||||||
view_holder_set_back_callback(submenu->view_holder, submenu_exit, submenu);
|
|
||||||
|
|
||||||
view_holder_set_view(submenu->view_holder, submenu_get_view(submenu->submenu));
|
|
||||||
api_lock_wait_unlock(submenu->lock);
|
|
||||||
|
|
||||||
view_holder_set_view(submenu->view_holder, NULL);
|
|
||||||
view_holder_free(submenu->view_holder);
|
|
||||||
furi_record_close(RECORD_GUI);
|
|
||||||
api_lock_free(submenu->lock);
|
|
||||||
|
|
||||||
submenu_reset(submenu->submenu);
|
|
||||||
if(submenu->accepted) {
|
|
||||||
mjs_return(mjs, mjs_mk_number(mjs, submenu->result));
|
|
||||||
} else {
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void* js_submenu_create(struct mjs* mjs, mjs_val_t* object) {
|
|
||||||
JsSubmenuInst* submenu = malloc(sizeof(JsSubmenuInst));
|
|
||||||
mjs_val_t submenu_obj = mjs_mk_object(mjs);
|
|
||||||
mjs_set(mjs, submenu_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, submenu));
|
|
||||||
mjs_set(mjs, submenu_obj, "addItem", ~0, MJS_MK_FN(js_submenu_add_item));
|
|
||||||
mjs_set(mjs, submenu_obj, "setHeader", ~0, MJS_MK_FN(js_submenu_set_header));
|
|
||||||
mjs_set(mjs, submenu_obj, "show", ~0, MJS_MK_FN(js_submenu_show));
|
|
||||||
submenu->submenu = submenu_alloc();
|
|
||||||
*object = submenu_obj;
|
|
||||||
return submenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_submenu_destroy(void* inst) {
|
|
||||||
JsSubmenuInst* submenu = inst;
|
|
||||||
submenu_free(submenu->submenu);
|
|
||||||
free(submenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const JsModuleDescriptor js_submenu_desc = {
|
|
||||||
"submenu",
|
|
||||||
js_submenu_create,
|
|
||||||
js_submenu_destroy,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor submenu_plugin_descriptor = {
|
|
||||||
.appid = PLUGIN_APP_ID,
|
|
||||||
.ep_api_version = PLUGIN_API_VERSION,
|
|
||||||
.entry_point = &js_submenu_desc,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlipperAppPluginDescriptor* js_submenu_ep(void) {
|
|
||||||
return &submenu_plugin_descriptor;
|
|
||||||
}
|
|
||||||
104
applications/system/js_app/modules/js_tests.c
Normal file
104
applications/system/js_app/modules/js_tests.c
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
5
applications/system/js_app/modules/js_tests.h
Normal file
5
applications/system/js_app/modules/js_tests.h
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../js_thread_i.h"
|
||||||
|
#include "../js_modules.h"
|
||||||
|
|
||||||
|
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
#include <gui/modules/text_box.h>
|
|
||||||
#include <gui/view_holder.h>
|
|
||||||
#include "../js_modules.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
TextBox* text_box;
|
|
||||||
ViewHolder* view_holder;
|
|
||||||
FuriString* text;
|
|
||||||
bool is_shown;
|
|
||||||
} JsTextboxInst;
|
|
||||||
|
|
||||||
static JsTextboxInst* get_this_ctx(struct mjs* mjs) {
|
|
||||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
|
||||||
JsTextboxInst* textbox = mjs_get_ptr(mjs, obj_inst);
|
|
||||||
furi_assert(textbox);
|
|
||||||
return textbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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_textbox_set_config(struct mjs* mjs) {
|
|
||||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 2)) return;
|
|
||||||
|
|
||||||
TextBoxFocus set_focus = TextBoxFocusStart;
|
|
||||||
mjs_val_t focus_arg = mjs_arg(mjs, 0);
|
|
||||||
const char* focus = mjs_get_string(mjs, &focus_arg, NULL);
|
|
||||||
if(!focus) {
|
|
||||||
ret_bad_args(mjs, "Focus must be a string");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if(!strncmp(focus, "start", strlen("start"))) {
|
|
||||||
set_focus = TextBoxFocusStart;
|
|
||||||
} else if(!strncmp(focus, "end", strlen("end"))) {
|
|
||||||
set_focus = TextBoxFocusEnd;
|
|
||||||
} else {
|
|
||||||
ret_bad_args(mjs, "Bad focus value");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextBoxFont set_font = TextBoxFontText;
|
|
||||||
mjs_val_t font_arg = mjs_arg(mjs, 1);
|
|
||||||
const char* font = mjs_get_string(mjs, &font_arg, NULL);
|
|
||||||
if(!font) {
|
|
||||||
ret_bad_args(mjs, "Font must be a string");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if(!strncmp(font, "text", strlen("text"))) {
|
|
||||||
set_font = TextBoxFontText;
|
|
||||||
} else if(!strncmp(font, "hex", strlen("hex"))) {
|
|
||||||
set_font = TextBoxFontHex;
|
|
||||||
} else {
|
|
||||||
ret_bad_args(mjs, "Bad font value");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text_box_set_focus(textbox->text_box, set_focus);
|
|
||||||
text_box_set_font(textbox->text_box, set_font);
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_textbox_add_text(struct mjs* mjs) {
|
|
||||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 1)) return;
|
|
||||||
|
|
||||||
mjs_val_t text_arg = mjs_arg(mjs, 0);
|
|
||||||
size_t text_len = 0;
|
|
||||||
const char* text = mjs_get_string(mjs, &text_arg, &text_len);
|
|
||||||
if(!text) {
|
|
||||||
ret_bad_args(mjs, "Text must be a string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid condition race between GUI and JS thread
|
|
||||||
text_box_set_text(textbox->text_box, "");
|
|
||||||
|
|
||||||
size_t new_len = furi_string_size(textbox->text) + text_len;
|
|
||||||
if(new_len >= 4096) {
|
|
||||||
furi_string_right(textbox->text, new_len / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
furi_string_cat(textbox->text, text);
|
|
||||||
|
|
||||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_textbox_clear_text(struct mjs* mjs) {
|
|
||||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 0)) return;
|
|
||||||
|
|
||||||
// Avoid condition race between GUI and JS thread
|
|
||||||
text_box_set_text(textbox->text_box, "");
|
|
||||||
|
|
||||||
furi_string_reset(textbox->text);
|
|
||||||
|
|
||||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_textbox_is_open(struct mjs* mjs) {
|
|
||||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 0)) return;
|
|
||||||
|
|
||||||
mjs_return(mjs, mjs_mk_boolean(mjs, textbox->is_shown));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void textbox_callback(void* context, uint32_t arg) {
|
|
||||||
UNUSED(arg);
|
|
||||||
JsTextboxInst* textbox = context;
|
|
||||||
view_holder_set_view(textbox->view_holder, NULL);
|
|
||||||
textbox->is_shown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void textbox_exit(void* context) {
|
|
||||||
JsTextboxInst* textbox = context;
|
|
||||||
// Using timer to schedule view_holder stop, will not work under high CPU load
|
|
||||||
furi_timer_pending_callback(textbox_callback, textbox, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_textbox_show(struct mjs* mjs) {
|
|
||||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 0)) return;
|
|
||||||
|
|
||||||
if(textbox->is_shown) {
|
|
||||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Textbox is already shown");
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
view_holder_set_view(textbox->view_holder, text_box_get_view(textbox->text_box));
|
|
||||||
textbox->is_shown = true;
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_textbox_close(struct mjs* mjs) {
|
|
||||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
|
||||||
if(!check_arg_count(mjs, 0)) return;
|
|
||||||
|
|
||||||
view_holder_set_view(textbox->view_holder, NULL);
|
|
||||||
textbox->is_shown = false;
|
|
||||||
|
|
||||||
mjs_return(mjs, MJS_UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void* js_textbox_create(struct mjs* mjs, mjs_val_t* object) {
|
|
||||||
JsTextboxInst* textbox = malloc(sizeof(JsTextboxInst));
|
|
||||||
|
|
||||||
mjs_val_t textbox_obj = mjs_mk_object(mjs);
|
|
||||||
mjs_set(mjs, textbox_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, textbox));
|
|
||||||
mjs_set(mjs, textbox_obj, "setConfig", ~0, MJS_MK_FN(js_textbox_set_config));
|
|
||||||
mjs_set(mjs, textbox_obj, "addText", ~0, MJS_MK_FN(js_textbox_add_text));
|
|
||||||
mjs_set(mjs, textbox_obj, "clearText", ~0, MJS_MK_FN(js_textbox_clear_text));
|
|
||||||
mjs_set(mjs, textbox_obj, "isOpen", ~0, MJS_MK_FN(js_textbox_is_open));
|
|
||||||
mjs_set(mjs, textbox_obj, "show", ~0, MJS_MK_FN(js_textbox_show));
|
|
||||||
mjs_set(mjs, textbox_obj, "close", ~0, MJS_MK_FN(js_textbox_close));
|
|
||||||
|
|
||||||
textbox->text = furi_string_alloc();
|
|
||||||
textbox->text_box = text_box_alloc();
|
|
||||||
|
|
||||||
Gui* gui = furi_record_open(RECORD_GUI);
|
|
||||||
textbox->view_holder = view_holder_alloc();
|
|
||||||
view_holder_attach_to_gui(textbox->view_holder, gui);
|
|
||||||
view_holder_set_back_callback(textbox->view_holder, textbox_exit, textbox);
|
|
||||||
|
|
||||||
*object = textbox_obj;
|
|
||||||
return textbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void js_textbox_destroy(void* inst) {
|
|
||||||
JsTextboxInst* textbox = inst;
|
|
||||||
|
|
||||||
view_holder_set_view(textbox->view_holder, NULL);
|
|
||||||
view_holder_free(textbox->view_holder);
|
|
||||||
textbox->view_holder = NULL;
|
|
||||||
|
|
||||||
furi_record_close(RECORD_GUI);
|
|
||||||
|
|
||||||
text_box_reset(textbox->text_box);
|
|
||||||
furi_string_reset(textbox->text);
|
|
||||||
|
|
||||||
text_box_free(textbox->text_box);
|
|
||||||
furi_string_free(textbox->text);
|
|
||||||
free(textbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const JsModuleDescriptor js_textbox_desc = {
|
|
||||||
"textbox",
|
|
||||||
js_textbox_create,
|
|
||||||
js_textbox_destroy,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const FlipperAppPluginDescriptor textbox_plugin_descriptor = {
|
|
||||||
.appid = PLUGIN_APP_ID,
|
|
||||||
.ep_api_version = PLUGIN_API_VERSION,
|
|
||||||
.entry_point = &js_textbox_desc,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlipperAppPluginDescriptor* js_textbox_ep(void) {
|
|
||||||
return &textbox_plugin_descriptor;
|
|
||||||
}
|
|
||||||
@@ -7,4 +7,5 @@
|
|||||||
static constexpr auto app_api_table = sort(create_array_t<sym_entry>(
|
static constexpr auto app_api_table = sort(create_array_t<sym_entry>(
|
||||||
API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)),
|
API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)),
|
||||||
API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)),
|
API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)),
|
||||||
API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t))));
|
API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)),
|
||||||
|
API_METHOD(js_module_get, void*, (JsModules*, const char*))));
|
||||||
|
|||||||
@@ -7,12 +7,16 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef void JsModules;
|
||||||
|
|
||||||
bool js_delay_with_flags(struct mjs* mjs, uint32_t time);
|
bool js_delay_with_flags(struct mjs* mjs, uint32_t time);
|
||||||
|
|
||||||
void js_flags_set(struct mjs* mjs, uint32_t flags);
|
void js_flags_set(struct mjs* mjs, uint32_t flags);
|
||||||
|
|
||||||
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout);
|
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout);
|
||||||
|
|
||||||
|
void* js_module_get(JsModules* modules, const char* name);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
81
applications/system/js_app/types/badusb/index.d.ts
vendored
Normal file
81
applications/system/js_app/types/badusb/index.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @brief Special key codes that this module recognizes
|
||||||
|
*/
|
||||||
|
export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI";
|
||||||
|
|
||||||
|
export type MainKey =
|
||||||
|
"DOWN" | "LEFT" | "RIGHT" | "UP" |
|
||||||
|
|
||||||
|
"ENTER" | "PAUSE" | "CAPSLOCK" | "DELETE" | "BACKSPACE" | "END" | "ESC" |
|
||||||
|
"HOME" | "INSERT" | "NUMLOCK" | "PAGEUP" | "PAGEDOWN" | "PRINTSCREEN" |
|
||||||
|
"SCROLLLOCK" | "SPACE" | "TAB" | "MENU" |
|
||||||
|
|
||||||
|
"F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" |
|
||||||
|
"F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" |
|
||||||
|
"F20" | "F21" | "F22" | "F23" | "F24" |
|
||||||
|
|
||||||
|
"\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" |
|
||||||
|
"+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" |
|
||||||
|
"]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" |
|
||||||
|
|
||||||
|
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" |
|
||||||
|
|
||||||
|
"A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" |
|
||||||
|
"M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" |
|
||||||
|
"Y" | "Z" |
|
||||||
|
|
||||||
|
"a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" |
|
||||||
|
"m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" |
|
||||||
|
"y" | "z";
|
||||||
|
|
||||||
|
export type KeyCode = MainKey | ModifierKey | number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initializes the module
|
||||||
|
* @param settings USB device settings. Omit to select default parameters
|
||||||
|
*/
|
||||||
|
export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tells whether the virtual USB HID device has successfully connected
|
||||||
|
*/
|
||||||
|
export declare function isConnected(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Presses one or multiple keys at once, then releases them
|
||||||
|
* @param keys The arguments represent a set of keys to. Out of that set, only
|
||||||
|
* one of the keys may represent a "main key" (see `MainKey`), with
|
||||||
|
* the rest being modifier keys (see `ModifierKey`).
|
||||||
|
*/
|
||||||
|
export declare function press(...keys: KeyCode[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Presses one or multiple keys at once without releasing them
|
||||||
|
* @param keys The arguments represent a set of keys to. Out of that set, only
|
||||||
|
* one of the keys may represent a "main key" (see `MainKey`), with
|
||||||
|
* the rest being modifier keys (see `ModifierKey`).
|
||||||
|
*/
|
||||||
|
export declare function hold(...keys: KeyCode[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Releases one or multiple keys at once
|
||||||
|
* @param keys The arguments represent a set of keys to. Out of that set, only
|
||||||
|
* one of the keys may represent a "main key" (see `MainKey`), with
|
||||||
|
* the rest being modifier keys (see `ModifierKey`).
|
||||||
|
*/
|
||||||
|
export declare function release(...keys: KeyCode[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prints a string by repeatedly pressing and releasing keys
|
||||||
|
* @param string The string to print
|
||||||
|
* @param delay How many milliseconds to wait between key presses
|
||||||
|
*/
|
||||||
|
export declare function print(string: string, delay?: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prints a string by repeatedly pressing and releasing keys. Presses
|
||||||
|
* "Enter" after printing the string
|
||||||
|
* @param string The string to print
|
||||||
|
* @param delay How many milliseconds to wait between key presses
|
||||||
|
*/
|
||||||
|
export declare function println(): void;
|
||||||
70
applications/system/js_app/types/event_loop/index.d.ts
vendored
Normal file
70
applications/system/js_app/types/event_loop/index.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
type Lit = undefined | null | {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription control interface
|
||||||
|
*/
|
||||||
|
export interface Subscription {
|
||||||
|
/**
|
||||||
|
* Cancels the subscription, preventing any future events managed by the
|
||||||
|
* subscription from firing
|
||||||
|
*/
|
||||||
|
cancel(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opaque event source identifier
|
||||||
|
*/
|
||||||
|
export type Contract<Item = undefined> = symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback can be assigned to an event loop to listen to an event. It may
|
||||||
|
* return an array with values that will be passed to it as arguments the next
|
||||||
|
* time that it is called. The first argument is always the subscription
|
||||||
|
* manager, and the second argument is always the item that trigged the event.
|
||||||
|
* The type of the item is defined by the event source.
|
||||||
|
*/
|
||||||
|
export type Callback<Item, Args extends Lit[]> = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to an event
|
||||||
|
* @param contract Event identifier
|
||||||
|
* @param callback Function to call when the event is triggered
|
||||||
|
* @param args Initial arguments passed to the callback
|
||||||
|
*/
|
||||||
|
export function subscribe<Item, Args extends Lit[]>(contract: Contract<Item>, callback: Callback<Item, Args>, ...args: Args): Subscription;
|
||||||
|
/**
|
||||||
|
* Runs the event loop until it is stopped (potentially never)
|
||||||
|
*/
|
||||||
|
export function run(): void | never;
|
||||||
|
/**
|
||||||
|
* Stops the event loop
|
||||||
|
*/
|
||||||
|
export function stop(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a timer event that can be subscribed to just like any other event
|
||||||
|
* @param mode Either `"oneshot"` or `"periodic"`
|
||||||
|
* @param interval Timer interval in milliseconds
|
||||||
|
*/
|
||||||
|
export function timer(mode: "oneshot" | "periodic", interval: number): Contract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message queue
|
||||||
|
*/
|
||||||
|
export interface Queue<T> {
|
||||||
|
/**
|
||||||
|
* Message event
|
||||||
|
*/
|
||||||
|
input: Contract<T>;
|
||||||
|
/**
|
||||||
|
* Sends a message to the queue
|
||||||
|
* @param message message to send
|
||||||
|
*/
|
||||||
|
send(message: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a message queue
|
||||||
|
* @param length maximum queue capacity
|
||||||
|
*/
|
||||||
|
export function queue<T>(length: number): Queue<T>;
|
||||||
14
applications/system/js_app/types/flipper/index.d.ts
vendored
Normal file
14
applications/system/js_app/types/flipper/index.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @brief Returns the device model
|
||||||
|
*/
|
||||||
|
export declare function getModel(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the name of the virtual dolphin
|
||||||
|
*/
|
||||||
|
export declare function getName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the battery charge percentage
|
||||||
|
*/
|
||||||
|
export declare function getBatteryCharge(): number;
|
||||||
178
applications/system/js_app/types/global.d.ts
vendored
Normal file
178
applications/system/js_app/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* @brief Pauses JavaScript execution for a while
|
||||||
|
* @param ms How many milliseconds to pause the execution for
|
||||||
|
*/
|
||||||
|
declare function delay(ms: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prints to the GUI console view
|
||||||
|
* @param args The arguments are converted to strings, concatenated without any
|
||||||
|
* spaces in between and printed to the console view
|
||||||
|
*/
|
||||||
|
declare function print(...args: any[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Converts a number to a string
|
||||||
|
* @param value The number to convert to a string
|
||||||
|
* @param base Integer base (`2`...`16`), default: 16
|
||||||
|
*/
|
||||||
|
declare function toString(value: number, base?: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads a JS value from a file
|
||||||
|
*
|
||||||
|
* Reads a file at the specified path, interprets it as a JS value and returns
|
||||||
|
* said value.
|
||||||
|
*
|
||||||
|
* @param path The path to the file
|
||||||
|
*/
|
||||||
|
declare function load(path: string): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief mJS Foreign Pointer type
|
||||||
|
*
|
||||||
|
* JavaScript code cannot do anything with values of `RawPointer` type except
|
||||||
|
* acquire them from native code and pass them right back to other parts of
|
||||||
|
* native code. These values cannot be turned into something meaningful, nor can
|
||||||
|
* be they modified.
|
||||||
|
*/
|
||||||
|
declare type RawPointer = symbol & { "__tag__": "raw_ptr" };
|
||||||
|
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Holds raw bytes
|
||||||
|
*/
|
||||||
|
declare class ArrayBuffer {
|
||||||
|
/**
|
||||||
|
* @brief The pointer to the byte buffer
|
||||||
|
* @note Like other `RawPointer` values, this value is essentially useless
|
||||||
|
* to JS code.
|
||||||
|
*/
|
||||||
|
getPtr: RawPointer;
|
||||||
|
/**
|
||||||
|
* @brief The length of the buffer in bytes
|
||||||
|
*/
|
||||||
|
byteLength: number;
|
||||||
|
/**
|
||||||
|
* @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer
|
||||||
|
* @param start The index of the byte in the source buffer to be used as the
|
||||||
|
* start for the new buffer
|
||||||
|
* @param end The index of the byte in the source buffer that follows the
|
||||||
|
* byte to be used as the last byte for the new buffer
|
||||||
|
*/
|
||||||
|
slice(start: number, end?: number): ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function ArrayBuffer(): ArrayBuffer;
|
||||||
|
|
||||||
|
declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32";
|
||||||
|
|
||||||
|
declare class TypedArray<E extends ElementType> {
|
||||||
|
/**
|
||||||
|
* @brief The length of the buffer in bytes
|
||||||
|
*/
|
||||||
|
byteLength: number;
|
||||||
|
/**
|
||||||
|
* @brief The length of the buffer in typed elements
|
||||||
|
*/
|
||||||
|
length: number;
|
||||||
|
/**
|
||||||
|
* @brief The underlying `ArrayBuffer`
|
||||||
|
*/
|
||||||
|
buffer: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class Uint8Array extends TypedArray<"u8"> { }
|
||||||
|
declare class Int8Array extends TypedArray<"i8"> { }
|
||||||
|
declare class Uint16Array extends TypedArray<"u16"> { }
|
||||||
|
declare class Int16Array extends TypedArray<"i16"> { }
|
||||||
|
declare class Uint32Array extends TypedArray<"u32"> { }
|
||||||
|
declare class Int32Array extends TypedArray<"i32"> { }
|
||||||
|
|
||||||
|
declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array;
|
||||||
|
declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array;
|
||||||
|
declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array;
|
||||||
|
declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array;
|
||||||
|
declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array;
|
||||||
|
declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array;
|
||||||
|
|
||||||
|
declare const console: {
|
||||||
|
/**
|
||||||
|
* @brief Prints to the UART logs at the `[I]` level
|
||||||
|
* @param args The arguments are converted to strings, concatenated without any
|
||||||
|
* spaces in between and printed to the logs
|
||||||
|
*/
|
||||||
|
log(...args: any[]): void;
|
||||||
|
/**
|
||||||
|
* @brief Prints to the UART logs at the `[D]` level
|
||||||
|
* @param args The arguments are converted to strings, concatenated without any
|
||||||
|
* spaces in between and printed to the logs
|
||||||
|
*/
|
||||||
|
debug(...args: any[]): void;
|
||||||
|
/**
|
||||||
|
* @brief Prints to the UART logs at the `[W]` level
|
||||||
|
* @param args The arguments are converted to strings, concatenated without any
|
||||||
|
* spaces in between and printed to the logs
|
||||||
|
*/
|
||||||
|
warn(...args: any[]): void;
|
||||||
|
/**
|
||||||
|
* @brief Prints to the UART logs at the `[E]` level
|
||||||
|
* @param args The arguments are converted to strings, concatenated without any
|
||||||
|
* spaces in between and printed to the logs
|
||||||
|
*/
|
||||||
|
error(...args: any[]): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class Array<T> {
|
||||||
|
/**
|
||||||
|
* @brief Takes items out of the array
|
||||||
|
*
|
||||||
|
* Removes elements from the array and returns them in a new array
|
||||||
|
*
|
||||||
|
* @param start The index to start taking elements from
|
||||||
|
* @param deleteCount How many elements to take
|
||||||
|
* @returns The elements that were taken out of the original array as a new
|
||||||
|
* array
|
||||||
|
*/
|
||||||
|
splice(start: number, deleteCount: number): T[];
|
||||||
|
/**
|
||||||
|
* @brief Adds a value to the end of the array
|
||||||
|
* @param value The value to add
|
||||||
|
* @returns New length of the array
|
||||||
|
*/
|
||||||
|
push(value: T): number;
|
||||||
|
/**
|
||||||
|
* @brief How many elements there are in the array
|
||||||
|
*/
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class String {
|
||||||
|
/**
|
||||||
|
* @brief How many characters there are in the string
|
||||||
|
*/
|
||||||
|
length: number;
|
||||||
|
/**
|
||||||
|
* @brief Returns the character code at an index in the string
|
||||||
|
* @param index The index to consult
|
||||||
|
*/
|
||||||
|
charCodeAt(index: number): number;
|
||||||
|
/**
|
||||||
|
* See `charCodeAt`
|
||||||
|
*/
|
||||||
|
at(index: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class Boolean { }
|
||||||
|
|
||||||
|
declare class Function { }
|
||||||
|
|
||||||
|
declare class Number { }
|
||||||
|
|
||||||
|
declare class Object { }
|
||||||
|
|
||||||
|
declare class RegExp { }
|
||||||
|
|
||||||
|
declare interface IArguments { }
|
||||||
|
|
||||||
|
declare type Partial<O extends object> = { [K in keyof O]?: O[K] };
|
||||||
45
applications/system/js_app/types/gpio/index.d.ts
vendored
Normal file
45
applications/system/js_app/types/gpio/index.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Contract } from "../event_loop";
|
||||||
|
|
||||||
|
export interface Mode {
|
||||||
|
direction: "in" | "out";
|
||||||
|
outMode?: "push_pull" | "open_drain";
|
||||||
|
inMode?: "analog" | "plain_digital" | "interrupt" | "event";
|
||||||
|
edge?: "rising" | "falling" | "both";
|
||||||
|
pull?: "up" | "down";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pin {
|
||||||
|
/**
|
||||||
|
* Configures a pin. This may be done several times.
|
||||||
|
* @param mode Pin configuration object
|
||||||
|
*/
|
||||||
|
init(mode: Mode): void;
|
||||||
|
/**
|
||||||
|
* Sets the output value of a pin if it's been configured with
|
||||||
|
* `direction: "out"`.
|
||||||
|
* @param value Logic value to output
|
||||||
|
*/
|
||||||
|
write(value: boolean): void;
|
||||||
|
/**
|
||||||
|
* Gets the input value of a pin if it's been configured with
|
||||||
|
* `direction: "in"`, but not `inMode: "analog"`.
|
||||||
|
*/
|
||||||
|
read(): boolean;
|
||||||
|
/**
|
||||||
|
* Gets the input voltage of a pin in millivolts if it's been configured
|
||||||
|
* with `direction: "in"` and `inMode: "analog"`
|
||||||
|
*/
|
||||||
|
read_analog(): number;
|
||||||
|
/**
|
||||||
|
* Returns an `event_loop` event that can be used to listen to interrupts,
|
||||||
|
* as configured by `init`
|
||||||
|
*/
|
||||||
|
interrupt(): Contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object that can be used to manage a GPIO pin. For the list of
|
||||||
|
* available pins, see https://docs.flipper.net/gpio-and-modules#miFsS
|
||||||
|
* @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`)
|
||||||
|
*/
|
||||||
|
export function get(pin: string | number): Pin;
|
||||||
16
applications/system/js_app/types/gui/dialog.d.ts
vendored
Normal file
16
applications/system/js_app/types/gui/dialog.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { View, ViewFactory } from ".";
|
||||||
|
import type { Contract } from "../event_loop";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
header: string,
|
||||||
|
text: string,
|
||||||
|
left: string,
|
||||||
|
center: string,
|
||||||
|
right: string,
|
||||||
|
}
|
||||||
|
declare class Dialog extends View<Props> {
|
||||||
|
input: Contract<"left" | "center" | "right">;
|
||||||
|
}
|
||||||
|
declare class DialogFactory extends ViewFactory<Props, Dialog> { }
|
||||||
|
declare const factory: DialogFactory;
|
||||||
|
export = factory;
|
||||||
7
applications/system/js_app/types/gui/empty_screen.d.ts
vendored
Normal file
7
applications/system/js_app/types/gui/empty_screen.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { View, ViewFactory } from ".";
|
||||||
|
|
||||||
|
type Props = {};
|
||||||
|
declare class EmptyScreen extends View<Props> { }
|
||||||
|
declare class EmptyScreenFactory extends ViewFactory<Props, EmptyScreen> { }
|
||||||
|
declare const factory: EmptyScreenFactory;
|
||||||
|
export = factory;
|
||||||
41
applications/system/js_app/types/gui/index.d.ts
vendored
Normal file
41
applications/system/js_app/types/gui/index.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Contract } from "../event_loop";
|
||||||
|
|
||||||
|
type Properties = { [K: string]: any };
|
||||||
|
|
||||||
|
export declare class View<Props extends Properties> {
|
||||||
|
set<P extends keyof Props>(property: P, value: Props[P]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class ViewFactory<Props extends Properties, V extends View<Props>> {
|
||||||
|
make(): V;
|
||||||
|
makeWith(initial: Partial<Props>): V;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ViewDispatcher {
|
||||||
|
/**
|
||||||
|
* Event source for `sendCustom` events
|
||||||
|
*/
|
||||||
|
custom: Contract<number>;
|
||||||
|
/**
|
||||||
|
* Event source for navigation events (back key presses)
|
||||||
|
*/
|
||||||
|
navigation: Contract;
|
||||||
|
/**
|
||||||
|
* Sends a number to the custom event handler
|
||||||
|
* @param event number to send
|
||||||
|
*/
|
||||||
|
sendCustom(event: number): void;
|
||||||
|
/**
|
||||||
|
* Switches to a view
|
||||||
|
* @param assoc View-ViewDispatcher association as returned by `add`
|
||||||
|
*/
|
||||||
|
switchTo(assoc: View<any>): void;
|
||||||
|
/**
|
||||||
|
* Sends this ViewDispatcher to the front or back, above or below all other
|
||||||
|
* GUI viewports
|
||||||
|
* @param direction Either `"front"` or `"back"`
|
||||||
|
*/
|
||||||
|
sendTo(direction: "front" | "back"): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const viewDispatcher: ViewDispatcher;
|
||||||
7
applications/system/js_app/types/gui/loading.d.ts
vendored
Normal file
7
applications/system/js_app/types/gui/loading.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { View, ViewFactory } from ".";
|
||||||
|
|
||||||
|
type Props = {};
|
||||||
|
declare class Loading extends View<Props> { }
|
||||||
|
declare class LoadingFactory extends ViewFactory<Props, Loading> { }
|
||||||
|
declare const factory: LoadingFactory;
|
||||||
|
export = factory;
|
||||||
13
applications/system/js_app/types/gui/submenu.d.ts
vendored
Normal file
13
applications/system/js_app/types/gui/submenu.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { View, ViewFactory } from ".";
|
||||||
|
import type { Contract } from "../event_loop";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
header: string,
|
||||||
|
items: string[],
|
||||||
|
};
|
||||||
|
declare class Submenu extends View<Props> {
|
||||||
|
chosen: Contract<number>;
|
||||||
|
}
|
||||||
|
declare class SubmenuFactory extends ViewFactory<Props, Submenu> { }
|
||||||
|
declare const factory: SubmenuFactory;
|
||||||
|
export = factory;
|
||||||
14
applications/system/js_app/types/gui/text_box.d.ts
vendored
Normal file
14
applications/system/js_app/types/gui/text_box.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { View, ViewFactory } from ".";
|
||||||
|
import type { Contract } from "../event_loop";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
text: string,
|
||||||
|
font: "text" | "hex",
|
||||||
|
focus: "start" | "end",
|
||||||
|
}
|
||||||
|
declare class TextBox extends View<Props> {
|
||||||
|
chosen: Contract<number>;
|
||||||
|
}
|
||||||
|
declare class TextBoxFactory extends ViewFactory<Props, TextBox> { }
|
||||||
|
declare const factory: TextBoxFactory;
|
||||||
|
export = factory;
|
||||||
14
applications/system/js_app/types/gui/text_input.d.ts
vendored
Normal file
14
applications/system/js_app/types/gui/text_input.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { View, ViewFactory } from ".";
|
||||||
|
import type { Contract } from "../event_loop";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
header: string,
|
||||||
|
minLength: number,
|
||||||
|
maxLength: number,
|
||||||
|
}
|
||||||
|
declare class TextInput extends View<Props> {
|
||||||
|
input: Contract<string>;
|
||||||
|
}
|
||||||
|
declare class TextInputFactory extends ViewFactory<Props, TextInput> { }
|
||||||
|
declare const factory: TextInputFactory;
|
||||||
|
export = factory;
|
||||||
24
applications/system/js_app/types/math/index.d.ts
vendored
Normal file
24
applications/system/js_app/types/math/index.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export function abs(n: number): number;
|
||||||
|
export function acos(n: number): number;
|
||||||
|
export function acosh(n: number): number;
|
||||||
|
export function asin(n: number): number;
|
||||||
|
export function asinh(n: number): number;
|
||||||
|
export function atan(n: number): number;
|
||||||
|
export function atan2(a: number, b: number): number;
|
||||||
|
export function atanh(n: number): number;
|
||||||
|
export function cbrt(n: number): number;
|
||||||
|
export function ceil(n: number): number;
|
||||||
|
export function clz32(n: number): number;
|
||||||
|
export function cos(n: number): number;
|
||||||
|
export function exp(n: number): number;
|
||||||
|
export function floor(n: number): number;
|
||||||
|
export function max(n: number, m: number): number;
|
||||||
|
export function min(n: number, m: number): number;
|
||||||
|
export function pow(n: number, m: number): number;
|
||||||
|
export function random(): number;
|
||||||
|
export function sign(n: number): number;
|
||||||
|
export function sin(n: number): number;
|
||||||
|
export function sqrt(n: number): number;
|
||||||
|
export function trunc(n: number): number;
|
||||||
|
declare const PI: number;
|
||||||
|
declare const EPSILON: number;
|
||||||
20
applications/system/js_app/types/notification/index.d.ts
vendored
Normal file
20
applications/system/js_app/types/notification/index.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* @brief Signals success to the user via the color LED, speaker and vibration
|
||||||
|
* motor
|
||||||
|
*/
|
||||||
|
export declare function success(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Signals failure to the user via the color LED, speaker and vibration
|
||||||
|
* motor
|
||||||
|
*/
|
||||||
|
export declare function error(): void;
|
||||||
|
|
||||||
|
export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Displays a basic color on the color LED
|
||||||
|
* @param color The color to display, see `Color`
|
||||||
|
* @param duration The duration, either `"short"` (10ms) or `"long"` (100ms)
|
||||||
|
*/
|
||||||
|
export declare function blink(color: Color, duration: "short" | "long"): void;
|
||||||
77
applications/system/js_app/types/serial/index.d.ts
vendored
Normal file
77
applications/system/js_app/types/serial/index.d.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* @brief Initializes the serial port
|
||||||
|
* @param port The port to initialize (`"lpuart"` or `"start"`)
|
||||||
|
* @param baudRate
|
||||||
|
*/
|
||||||
|
export declare function setup(port: "lpuart" | "usart", baudRate: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes data to the serial port
|
||||||
|
* @param value The data to write:
|
||||||
|
* - Strings will get sent as ASCII.
|
||||||
|
* - Numbers will get sent as a single byte.
|
||||||
|
* - Arrays of numbers will get sent as a sequence of bytes.
|
||||||
|
* - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence
|
||||||
|
* of bytes.
|
||||||
|
*/
|
||||||
|
export declare function write<E extends ElementType>(value: string | number | number[] | ArrayBuffer | TypedArray<E>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads data from the serial port
|
||||||
|
* @param length The number of bytes to read
|
||||||
|
* @param timeout The number of time, in milliseconds, after which this function
|
||||||
|
* will give up and return what it read up to that point. If
|
||||||
|
* unset, the function will wait forever.
|
||||||
|
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
|
||||||
|
* were read.
|
||||||
|
*/
|
||||||
|
export declare function read(length: number, timeout?: number): string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads data from the serial port
|
||||||
|
*
|
||||||
|
* Data is read one character after another until either a `\r` or `\n`
|
||||||
|
* character is received, neither of which is included in the result.
|
||||||
|
*
|
||||||
|
* @param timeout The number of time, in milliseconds, after which this function
|
||||||
|
* will give up and return what it read up to that point. If
|
||||||
|
* unset, the function will wait forever. The timeout only
|
||||||
|
* applies to characters, not entire strings.
|
||||||
|
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
|
||||||
|
* were read.
|
||||||
|
*/
|
||||||
|
export declare function readln(timeout?: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads data from the serial port
|
||||||
|
* @param length The number of bytes to read
|
||||||
|
* @param timeout The number of time, in milliseconds, after which this function
|
||||||
|
* will give up and return what it read up to that point. If
|
||||||
|
* unset, the function will wait forever.
|
||||||
|
* @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were
|
||||||
|
* read.
|
||||||
|
*/
|
||||||
|
export declare function readBytes(length: number, timeout?: number): ArrayBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads data from the serial port, trying to match it to a pattern
|
||||||
|
* @param patterns A single pattern or an array of patterns:
|
||||||
|
* - If the argument is a single `string`, this function will
|
||||||
|
* match against the given string.
|
||||||
|
* - If the argument is an array of `number`s, this function
|
||||||
|
* will match against the given sequence of bytes,
|
||||||
|
* - If the argument is an array of `string`s, this function
|
||||||
|
* will match against any string out of the ones that were
|
||||||
|
* provided.
|
||||||
|
* - If the argument is an array of arrays of `number`s, this
|
||||||
|
* function will match against any sequence of bytes out of
|
||||||
|
* the ones that were provided.
|
||||||
|
* @param timeout The number of time, in milliseconds, after which this function
|
||||||
|
* will give up and return what it read up to that point. If
|
||||||
|
* unset, the function will wait forever. The timeout only
|
||||||
|
* applies to characters, not entire strings.
|
||||||
|
* @returns The index of the matched pattern if multiple were provided, or 0 if
|
||||||
|
* only one was provided and it matched, or `undefined` if none of the
|
||||||
|
* patterns matched.
|
||||||
|
*/
|
||||||
|
export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined;
|
||||||
237
applications/system/js_app/types/storage/index.d.ts
vendored
Normal file
237
applications/system/js_app/types/storage/index.d.ts
vendored
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* File readability mode:
|
||||||
|
* - `"r"`: read-only
|
||||||
|
* - `"w"`: write-only
|
||||||
|
* - `"rw"`: read-write
|
||||||
|
*/
|
||||||
|
export type AccessMode = "r" | "w" | "rw";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File creation mode:
|
||||||
|
* - `"open_existing"`: open file or fail if it doesn't exist
|
||||||
|
* - `"open_always"`: open file or create a new empty one if it doesn't exist
|
||||||
|
* - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist
|
||||||
|
* - `"create_new"`: create new file or fail if it exists
|
||||||
|
* - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist
|
||||||
|
*/
|
||||||
|
export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always";
|
||||||
|
|
||||||
|
/** Standard UNIX timestamp */
|
||||||
|
export type Timestamp = number;
|
||||||
|
|
||||||
|
/** File information structure */
|
||||||
|
export declare class FileInfo {
|
||||||
|
/**
|
||||||
|
* Full path (e.g. "/ext/test", returned by `stat`) or file name
|
||||||
|
* (e.g. "test", returned by `readDirectory`)
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* Is the file a directory?
|
||||||
|
*/
|
||||||
|
isDirectory: boolean;
|
||||||
|
/**
|
||||||
|
* File size in bytes, or 0 in the case of directories
|
||||||
|
*/
|
||||||
|
size: number;
|
||||||
|
/**
|
||||||
|
* Time of last access as a UNIX timestamp
|
||||||
|
*/
|
||||||
|
accessTime: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filesystem information structure */
|
||||||
|
export declare class FsInfo {
|
||||||
|
/** Total size of the filesystem, in bytes */
|
||||||
|
totalSpace: number;
|
||||||
|
/** Free space in the filesystem, in bytes */
|
||||||
|
freeSpace: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// file operations
|
||||||
|
|
||||||
|
/** File class */
|
||||||
|
export declare class File {
|
||||||
|
/**
|
||||||
|
* Closes the file. After this method is called, all other operations
|
||||||
|
* related to this file become unavailable.
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
close(): boolean;
|
||||||
|
/**
|
||||||
|
* Is the file currently open?
|
||||||
|
*/
|
||||||
|
isOpen(): boolean;
|
||||||
|
/**
|
||||||
|
* Reads bytes from a file opened in read-only or read-write mode
|
||||||
|
* @param mode The data type to interpret the bytes as: a `string` decoded
|
||||||
|
* from ASCII data (`"ascii"`), or an `ArrayBuf` (`"binary"`)
|
||||||
|
* @param bytes How many bytes to read from the file
|
||||||
|
* @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode
|
||||||
|
* is `ascii`. The number of bytes that was actually read may be
|
||||||
|
* fewer than requested.
|
||||||
|
*/
|
||||||
|
read<T extends ArrayBuffer | string>(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T;
|
||||||
|
/**
|
||||||
|
* Writes bytes to a file opened in write-only or read-write mode
|
||||||
|
* @param data The data to write: a string that will be ASCII-encoded, or an
|
||||||
|
* ArrayBuf
|
||||||
|
* @returns the amount of bytes that was actually written
|
||||||
|
*/
|
||||||
|
write(data: ArrayBuffer | string): number;
|
||||||
|
/**
|
||||||
|
* Moves the R/W pointer forward
|
||||||
|
* @param bytes How many bytes to move the pointer forward by
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
seekRelative(bytes: number): boolean;
|
||||||
|
/**
|
||||||
|
* Moves the R/W pointer to an absolute position inside the file
|
||||||
|
* @param bytes The position inside the file
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
seekAbsolute(bytes: number): boolean;
|
||||||
|
/**
|
||||||
|
* Gets the absolute position of the R/W pointer in bytes
|
||||||
|
*/
|
||||||
|
tell(): number;
|
||||||
|
/**
|
||||||
|
* Discards the data after the current position of the R/W pointer in a file
|
||||||
|
* opened in either write-only or read-write mode.
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
truncate(): boolean;
|
||||||
|
/**
|
||||||
|
* Reads the total size of the file in bytes
|
||||||
|
*/
|
||||||
|
size(): number;
|
||||||
|
/**
|
||||||
|
* Detects whether the R/W pointer has reached the end of the file
|
||||||
|
*/
|
||||||
|
eof(): boolean;
|
||||||
|
/**
|
||||||
|
* Copies bytes from the R/W pointer in the current file to the R/W pointer
|
||||||
|
* in another file
|
||||||
|
* @param dest The file to copy the bytes into
|
||||||
|
* @param bytes The number of bytes to copy
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
copyTo(dest: File, bytes: number): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file
|
||||||
|
* @param path The path to the file
|
||||||
|
* @param accessMode `"r"`, `"w"` or `"rw"`; see `AccessMode`
|
||||||
|
* @param openMode `"open_existing"`, `"open_always"`, `"open_append"`,
|
||||||
|
* `"create_new"` or `"create_always"`; see `OpenMode`
|
||||||
|
* @returns a `File` on success, or `undefined` on failure
|
||||||
|
*/
|
||||||
|
export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined;
|
||||||
|
/**
|
||||||
|
* Detects whether a file exists
|
||||||
|
* @param path The path to the file
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
export declare function fileExists(path: string): boolean;
|
||||||
|
|
||||||
|
// directory operations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the list of files in a directory
|
||||||
|
* @param path The path to the directory
|
||||||
|
* @returns Array of `FileInfo` structures with directory entries,
|
||||||
|
* or `undefined` on failure
|
||||||
|
*/
|
||||||
|
export declare function readDirectory(path: string): FileInfo[] | undefined;
|
||||||
|
/**
|
||||||
|
* Detects whether a directory exists
|
||||||
|
* @param path The path to the directory
|
||||||
|
*/
|
||||||
|
export declare function directoryExists(path: string): boolean;
|
||||||
|
/**
|
||||||
|
* Creates an empty directory
|
||||||
|
* @param path The path to the new directory
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
export declare function makeDirectory(path: string): boolean;
|
||||||
|
|
||||||
|
// common (file/dir) operations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether a file or a directory exists
|
||||||
|
* @param path The path to the file or directory
|
||||||
|
*/
|
||||||
|
export declare function fileOrDirExists(path: string): boolean;
|
||||||
|
/**
|
||||||
|
* Acquires metadata about a file or directory
|
||||||
|
* @param path The path to the file or directory
|
||||||
|
* @returns A `FileInfo` structure or `undefined` on failure
|
||||||
|
*/
|
||||||
|
export declare function stat(path: string): FileInfo | undefined;
|
||||||
|
/**
|
||||||
|
* Removes a file or an empty directory
|
||||||
|
* @param path The path to the file or directory
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
export declare function remove(path: string): boolean;
|
||||||
|
/**
|
||||||
|
* Removes a file or recursively removes a possibly non-empty directory
|
||||||
|
* @param path The path to the file or directory
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
export declare function rmrf(path: string): boolean;
|
||||||
|
/**
|
||||||
|
* Renames or moves a file or directory
|
||||||
|
* @param oldPath The old path to the file or directory
|
||||||
|
* @param newPath The new path that the file or directory will become accessible
|
||||||
|
* under
|
||||||
|
* @returns `true` on success, `false` on failure
|
||||||
|
*/
|
||||||
|
export declare function rename(oldPath: string, newPath: string): boolean;
|
||||||
|
/**
|
||||||
|
* Copies a file or recursively copies a possibly non-empty directory
|
||||||
|
* @param oldPath The original path to the file or directory
|
||||||
|
* @param newPath The new path that the copy of the file or directory will be
|
||||||
|
* accessible under
|
||||||
|
*/
|
||||||
|
export declare function copy(oldPath: string, newPath: string): boolean;
|
||||||
|
/**
|
||||||
|
* Fetches generic information about a filesystem
|
||||||
|
* @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`)
|
||||||
|
*/
|
||||||
|
export declare function fsInfo(filesystem: string): FsInfo | undefined;
|
||||||
|
/**
|
||||||
|
* Chooses the next available filename with a numeric suffix in a directory
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* "/ext/example_dir/example_file123.txt"
|
||||||
|
* \______________/ \__________/\_/\__/
|
||||||
|
* dirPath fileName | |
|
||||||
|
* | +---- fileExt
|
||||||
|
* +------- selected by this function
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param dirPath The directory to look in
|
||||||
|
* @param fileName The base of the filename (the part before the numeric suffix)
|
||||||
|
* @param fileExt The extension of the filename (the part after the numeric suffix)
|
||||||
|
* @param maxLen The maximum length of the filename with the numeric suffix
|
||||||
|
* @returns The base of the filename with the next available numeric suffix,
|
||||||
|
* without the extension or the base directory.
|
||||||
|
*/
|
||||||
|
export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string;
|
||||||
|
|
||||||
|
// path operations that do not access the filesystem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the two paths are equivalent. Respects filesystem-defined
|
||||||
|
* path equivalence rules.
|
||||||
|
*/
|
||||||
|
export declare function arePathsEqual(path1: string, path2: string): boolean;
|
||||||
|
/**
|
||||||
|
* Determines whether a path is a subpath of another path. Respects
|
||||||
|
* filesystem-defined path equivalence rules.
|
||||||
|
* @param parentPath The parent path
|
||||||
|
* @param childPath The child path
|
||||||
|
*/
|
||||||
|
export declare function isSubpathOf(parentPath: string, childPath: string): boolean;
|
||||||
8
applications/system/js_app/types/tests/index.d.ts
vendored
Normal file
8
applications/system/js_app/types/tests/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Unit test module. Only available if the firmware has been configured with
|
||||||
|
* `FIRMWARE_APP_SET=unit_tests`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function fail(message: string): never;
|
||||||
|
export function assert_eq<T>(expected: T, result: T): void | never;
|
||||||
|
export function assert_float_close(expected: number, result: number, epsilon: number): void | never;
|
||||||
Reference in New Issue
Block a user