This commit is contained in:
Willy-JL
2024-02-12 15:52:34 +00:00
119 changed files with 17570 additions and 36 deletions

View File

@@ -86,7 +86,10 @@ Small applications providing configuration for basic firmware and its services.
## system
Utility apps not visible in other menus.
Utility apps not visible in other menus, plus few external apps pre-packaged with the firmware.
- `hid_app` - BLE & USB HID remote
- `js_app` - JS engine runner
- `snake_game` - Snake game
- `storage_move_to_sd` - Data migration tool for internal storage
- `updater` - Update service & application

View File

@@ -34,6 +34,7 @@ static const char* known_ext[] = {
[ArchiveFileTypeBadKb] = ".txt",
[ArchiveFileTypeU2f] = "?",
[ArchiveFileTypeApplication] = ".fap",
[ArchiveFileTypeJS] = ".js",
[ArchiveFileTypeSearch] = "*",
[ArchiveFileTypeUpdateManifest] = ".fuf",
[ArchiveFileTypeFolder] = "?",

View File

@@ -21,6 +21,7 @@ typedef enum {
ArchiveFileTypeBadKb,
ArchiveFileTypeU2f,
ArchiveFileTypeApplication,
ArchiveFileTypeJS,
ArchiveFileTypeSearch,
ArchiveFileTypeUpdateManifest,
ArchiveFileTypeFolder,

View File

@@ -36,6 +36,8 @@ const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) {
return "U2F";
case ArchiveFileTypeUpdateManifest:
return "UpdaterApp";
case ArchiveFileTypeJS:
return "JS Runner";
default:
return NULL;
}

View File

@@ -35,6 +35,7 @@ static const Icon* ArchiveItemIcons[] = {
[ArchiveFileTypeBadKb] = &I_badkb_10px,
[ArchiveFileTypeU2f] = &I_u2f_10px,
[ArchiveFileTypeApplication] = &I_Apps_10px,
[ArchiveFileTypeJS] = &I_js_script_10px,
[ArchiveFileTypeSearch] = &I_search_10px,
[ArchiveFileTypeUpdateManifest] = &I_update_10px,
[ArchiveFileTypeFolder] = &I_dir_10px,

View File

@@ -32,12 +32,12 @@ typedef enum {
(WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit | \
WorkerEvtFolderRefresh | WorkerEvtConfigChange)
ARRAY_DEF(_idx_last_array, int32_t) // Unused, kept for compatibility
ARRAY_DEF(_IdxLastArray, int32_t) // Unused, kept for compatibility
ARRAY_DEF(ExtFilterArray, FuriString*, FURI_STRING_OPLIST)
struct BrowserWorker {
FuriThread* thread;
FuriString* filter_extension;
FuriString* path_start;
FuriString* path_current;
FuriString* path_next;
@@ -46,7 +46,8 @@ struct BrowserWorker {
uint32_t load_count;
bool skip_assets;
bool hide_dot_files;
_idx_last_array_t _idx_last; // Unused, kept for compatibility
_IdxLastArray_t _idx_last; // Unused, kept for compatibility
ExtFilterArray_t ext_filter;
void* cb_ctx;
BrowserWorkerFolderOpenCallback folder_cb;
@@ -55,6 +56,7 @@ struct BrowserWorker {
BrowserWorkerLongLoadCallback long_load_cb;
bool keep_selection;
FuriString* passed_ext_filter;
};
static bool browser_path_is_file(FuriString* path) {
@@ -80,6 +82,31 @@ static bool browser_path_trim(FuriString* path) {
}
return is_root;
}
static void browser_parse_ext_filter(ExtFilterArray_t ext_filter, const char* filter_str) {
if(!filter_str) {
return;
}
size_t len = strlen(filter_str);
if(len == 0) {
return;
}
size_t str_offset = 0;
FuriString* ext_temp = furi_string_alloc();
while(1) {
size_t ext_len = strcspn(&filter_str[str_offset], "|");
furi_string_set_strn(ext_temp, &filter_str[str_offset], ext_len);
ExtFilterArray_push_back(ext_filter, ext_temp);
str_offset += ext_len + 1;
if(str_offset >= len) {
break;
}
}
furi_string_free(ext_temp);
}
static bool browser_filter_by_name(BrowserWorker* browser, FuriString* name, bool is_folder) {
// Skip dot files if enabled
@@ -98,12 +125,20 @@ static bool browser_filter_by_name(BrowserWorker* browser, FuriString* name, boo
}
} else {
// Filter files by extension
if((furi_string_empty(browser->filter_extension)) ||
(furi_string_cmp_str(browser->filter_extension, "*") == 0)) {
if(ExtFilterArray_size(browser->ext_filter) == 0) {
return true;
}
if(furi_string_end_with(name, browser->filter_extension)) {
return true;
ExtFilterArray_it_t it;
for(ExtFilterArray_it(it, browser->ext_filter); !ExtFilterArray_end_p(it);
ExtFilterArray_next(it)) {
FuriString* ext = *ExtFilterArray_cref(it);
if((furi_string_empty(ext)) || (furi_string_cmp_str(ext, "*") == 0)) {
return true;
}
if(furi_string_end_with(name, ext)) {
return true;
}
}
}
return false;
@@ -460,14 +495,17 @@ static int32_t browser_worker(void* context) {
BrowserWorker* file_browser_worker_alloc(
FuriString* path,
const char* base_path,
const char* filter_ext,
const char* ext_filter,
bool skip_assets,
bool hide_dot_files) {
BrowserWorker* browser = malloc(sizeof(BrowserWorker));
browser->filter_extension = furi_string_alloc_set(filter_ext);
ExtFilterArray_init(browser->ext_filter);
browser_parse_ext_filter(browser->ext_filter, ext_filter);
browser->skip_assets = skip_assets;
browser->hide_dot_files = hide_dot_files;
browser->passed_ext_filter = furi_string_alloc_set(ext_filter);
browser->path_current = furi_string_alloc_set(path);
browser->path_next = furi_string_alloc_set(path);
@@ -490,10 +528,12 @@ void file_browser_worker_free(BrowserWorker* browser) {
furi_thread_join(browser->thread);
furi_thread_free(browser->thread);
furi_string_free(browser->filter_extension);
furi_string_free(browser->path_next);
furi_string_free(browser->path_current);
furi_string_free(browser->path_start);
furi_string_free(browser->passed_ext_filter);
ExtFilterArray_clear(browser->ext_filter);
free(browser);
}
@@ -534,13 +574,13 @@ void file_browser_worker_set_long_load_callback(
void file_browser_worker_set_config(
BrowserWorker* browser,
FuriString* path,
const char* filter_ext,
const char* ext_filter,
bool skip_assets,
bool hide_dot_files) {
furi_assert(browser);
furi_string_set(browser->path_next, path);
browser->keep_selection = false;
furi_string_set(browser->filter_extension, filter_ext);
browser_parse_ext_filter(browser->ext_filter, ext_filter);
browser->skip_assets = skip_assets;
browser->hide_dot_files = hide_dot_files;
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtConfigChange);
@@ -548,7 +588,7 @@ void file_browser_worker_set_config(
const char* file_browser_worker_get_filter_ext(BrowserWorker* browser) {
furi_assert(browser);
return furi_string_get_cstr(browser->filter_extension);
return furi_string_get_cstr(browser->passed_ext_filter);
}
void file_browser_worker_set_filter_ext(
@@ -558,7 +598,7 @@ void file_browser_worker_set_filter_ext(
furi_assert(browser);
furi_string_set(browser->path_next, path);
browser->keep_selection = true;
furi_string_set(browser->filter_extension, filter_ext);
furi_string_set(browser->passed_ext_filter, filter_ext);
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtConfigChange);
}

View File

@@ -26,7 +26,7 @@ typedef void (*BrowserWorkerLongLoadCallback)(void* context);
BrowserWorker* file_browser_worker_alloc(
FuriString* path,
const char* base_path,
const char* filter_ext,
const char* ext_filter,
bool skip_assets,
bool hide_dot_files);
@@ -53,7 +53,7 @@ void file_browser_worker_set_long_load_callback(
void file_browser_worker_set_config(
BrowserWorker* browser,
FuriString* path,
const char* filter_ext,
const char* ext_filter,
bool skip_assets,
bool hide_dot_files);

View File

@@ -7,9 +7,12 @@
#include <gui/view_holder.h>
#include <gui/modules/loading.h>
#include <dolphin/dolphin.h>
#include <lib/toolbox/path.h>
#define TAG "LoaderApplications"
#define JS_RUNNER_APP "JS Runner"
struct LoaderApplications {
FuriThread* thread;
void (*closed_cb)(void*);
@@ -36,7 +39,7 @@ void loader_applications_free(LoaderApplications* loader_applications) {
}
typedef struct {
FuriString* fap_path;
FuriString* file_path;
DialogsApp* dialogs;
Storage* storage;
Loader* loader;
@@ -48,7 +51,7 @@ typedef struct {
static LoaderApplicationsApp* loader_applications_app_alloc() {
LoaderApplicationsApp* app = malloc(sizeof(LoaderApplicationsApp)); //-V799
app->fap_path = furi_string_alloc_set(EXT_PATH("apps"));
app->file_path = furi_string_alloc_set(EXT_PATH("apps"));
app->dialogs = furi_record_open(RECORD_DIALOGS);
app->storage = furi_record_open(RECORD_STORAGE);
app->loader = furi_record_open(RECORD_LOADER);
@@ -73,7 +76,7 @@ static void loader_applications_app_free(LoaderApplicationsApp* app) {
furi_record_close(RECORD_LOADER);
furi_record_close(RECORD_DIALOGS);
furi_record_close(RECORD_STORAGE);
furi_string_free(app->fap_path);
furi_string_free(app->file_path);
free(app);
}
@@ -84,13 +87,19 @@ static bool loader_applications_item_callback(
FuriString* item_name) {
LoaderApplicationsApp* loader_applications_app = context;
furi_assert(loader_applications_app);
return flipper_application_load_name_and_icon(
path, loader_applications_app->storage, icon_ptr, item_name);
if(furi_string_end_with(path, ".fap")) {
return flipper_application_load_name_and_icon(
path, loader_applications_app->storage, icon_ptr, item_name);
} else {
path_extract_filename(path, item_name, false);
memcpy(*icon_ptr, icon_get_data(&I_js_script_10px), FAP_MANIFEST_MAX_ICON_SIZE);
return true;
}
}
static bool loader_applications_select_app(LoaderApplicationsApp* loader_applications_app) {
const DialogsFileBrowserOptions browser_options = {
.extension = ".fap",
.extension = ".fap|.js",
.skip_assets = true,
.icon = &I_unknown_10px,
.hide_ext = true,
@@ -101,8 +110,8 @@ static bool loader_applications_select_app(LoaderApplicationsApp* loader_applica
return dialog_file_browser_show(
loader_applications_app->dialogs,
loader_applications_app->fap_path,
loader_applications_app->fap_path,
loader_applications_app->file_path,
loader_applications_app->file_path,
&browser_options);
}
@@ -117,11 +126,10 @@ static void loader_pubsub_callback(const void* message, void* context) {
}
}
static void loader_applications_start_app(LoaderApplicationsApp* app) {
const char* name = furi_string_get_cstr(app->fap_path);
if(!furi_string_start_with_str(app->fap_path, EXT_PATH("apps/Games/")) &&
!furi_string_start_with_str(app->fap_path, EXT_PATH("apps/Media/"))) {
static void
loader_applications_start_app(LoaderApplicationsApp* app, const char* name, const char* args) {
if(!furi_string_start_with_str(app->file_path, EXT_PATH("apps/Games/")) &&
!furi_string_start_with_str(app->file_path, EXT_PATH("apps/Media/"))) {
dolphin_deed(DolphinDeedPluginInternalStart);
}
@@ -130,7 +138,7 @@ static void loader_applications_start_app(LoaderApplicationsApp* app) {
FuriPubSubSubscription* subscription =
furi_pubsub_subscribe(loader_get_pubsub(app->loader), loader_pubsub_callback, thread_id);
LoaderStatus status = loader_start_with_gui_error(app->loader, name, NULL);
LoaderStatus status = loader_start_with_gui_error(app->loader, name, args);
if(status == LoaderStatusOk) {
furi_thread_flags_wait(APPLICATION_STOP_EVENT, FuriFlagWaitAny, FuriWaitForever);
@@ -147,7 +155,12 @@ static int32_t loader_applications_thread(void* p) {
view_holder_start(app->view_holder);
while(loader_applications_select_app(app)) {
loader_applications_start_app(app);
if(!furi_string_end_with(app->file_path, ".js")) {
loader_applications_start_app(app, furi_string_get_cstr(app->file_path), NULL);
} else {
loader_applications_start_app(
app, JS_RUNNER_APP, furi_string_get_cstr(app->file_path));
}
}
// stop loading animation

View File

@@ -5,6 +5,7 @@ App(
provides=[
"updater_app",
"storage_move_to_sd",
"js_app",
# "archive",
],
)

View File

@@ -0,0 +1,41 @@
App(
appid="js_app",
name="JS Runner",
apptype=FlipperAppType.SYSTEM,
entry_point="js_app",
stack_size=2 * 1024,
resources="examples",
order=0,
)
App(
appid="js_dialog",
apptype=FlipperAppType.PLUGIN,
entry_point="js_dialog_ep",
requires=["js_app"],
sources=["modules/js_dialog.c"],
)
App(
appid="js_notification",
apptype=FlipperAppType.PLUGIN,
entry_point="js_notification_ep",
requires=["js_app"],
sources=["modules/js_notification.c"],
)
App(
appid="js_badusb",
apptype=FlipperAppType.PLUGIN,
entry_point="js_badusb_ep",
requires=["js_app"],
sources=["modules/js_badusb.c"],
)
App(
appid="js_uart",
apptype=FlipperAppType.PLUGIN,
entry_point="js_uart_ep",
requires=["js_app"],
sources=["modules/js_uart.c"],
)

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
let badusb = require("badusb");
let notify = require("notification");
let flipper = require("flipper");
let dialog = require("dialog");
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper", prod_name: "Zero" });
dialog.message("BadUSB demo", "Press OK to start");
if (badusb.isConnected()) {
notify.blink("green", "short");
print("USB is connected");
badusb.println("Hello, world!");
badusb.press("CTRL", "a");
badusb.press("CTRL", "c");
badusb.press("DOWN");
delay(1000);
badusb.press("CTRL", "v");
delay(1000);
badusb.press("CTRL", "v");
badusb.println("1234", 200);
badusb.println("Flipper Model: " + flipper.getModel());
badusb.println("Flipper Name: " + flipper.getName());
badusb.println("Battery level: " + to_string(flipper.getBatteryCharge()) + "%");
notify.success();
} else {
print("USB not connected");
notify.error();
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
let dialog = require("dialog");
let result1 = dialog.message("Dialog demo", "Press OK to start");
print(result1);
let dialog_params = ({
header: "Test_header",
text: "Test_text",
button_left: "Left",
button_right: "Right",
button_center: "OK"
});
let result2 = dialog.custom(dialog_params);
if (result2 === "") {
print("Back is pressed");
} else {
print(result2, "is pressed");
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
let uart = require("uart");
uart.setup(115200);
while (1) {
let rx_data = uart.readBytes(1, 0);
if (rx_data !== undefined) {
uart.write(rx_data);
let data_view = Uint8Array(rx_data);
print("0x" + to_hex_string(data_view[0]));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,131 @@
#include <dialogs/dialogs.h>
#include "js_thread.h"
#include <storage/storage.h>
#include "js_app_i.h"
#include <toolbox/path.h>
#include <assets_icons.h>
#define TAG "JS app"
typedef struct {
JsThread* js_thread;
Gui* gui;
ViewDispatcher* view_dispatcher;
Loading* loading;
JsConsoleView* console_view;
} JsApp;
static uint32_t js_view_exit(void* context) {
UNUSED(context);
return VIEW_NONE;
}
static void js_app_compact_trace(FuriString* trace_str) {
// Keep only first line
size_t line_end = furi_string_search_char(trace_str, '\n');
if(line_end > 0) {
furi_string_left(trace_str, line_end);
}
// Remove full path
FuriString* file_name = furi_string_alloc();
size_t filename_start = furi_string_search_rchar(trace_str, '/');
if(filename_start > 0) {
filename_start++;
furi_string_set_n(
file_name, trace_str, filename_start, furi_string_size(trace_str) - filename_start);
furi_string_printf(trace_str, "at %s", furi_string_get_cstr(file_name));
}
furi_string_free(file_name);
}
static void js_callback(JsThreadEvent event, const char* msg, void* context) {
JsApp* app = context;
furi_assert(app);
if(event == JsThreadEventDone) {
FURI_LOG_I(TAG, "Script done");
console_view_print(app->console_view, "--- DONE ---");
} else if(event == JsThreadEventPrint) {
console_view_print(app->console_view, msg);
} else if(event == JsThreadEventError) {
console_view_print(app->console_view, "--- ERROR ---");
console_view_print(app->console_view, msg);
} else if(event == JsThreadEventErrorTrace) {
FuriString* compact_trace = furi_string_alloc_set_str(msg);
js_app_compact_trace(compact_trace);
console_view_print(app->console_view, furi_string_get_cstr(compact_trace));
furi_string_free(compact_trace);
console_view_print(app->console_view, "See logs for full trace");
}
}
static JsApp* js_app_alloc(void) {
JsApp* app = malloc(sizeof(JsApp));
app->view_dispatcher = view_dispatcher_alloc();
app->loading = loading_alloc();
app->gui = furi_record_open("gui");
view_dispatcher_enable_queue(app->view_dispatcher);
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
view_dispatcher_add_view(
app->view_dispatcher, JsAppViewLoading, loading_get_view(app->loading));
app->console_view = console_view_alloc();
view_dispatcher_add_view(
app->view_dispatcher, JsAppViewConsole, console_view_get_view(app->console_view));
view_set_previous_callback(console_view_get_view(app->console_view), js_view_exit);
view_dispatcher_switch_to_view(app->view_dispatcher, JsAppViewConsole);
return app;
}
static void js_app_free(JsApp* app) {
console_view_free(app->console_view);
view_dispatcher_remove_view(app->view_dispatcher, JsAppViewConsole);
loading_free(app->loading);
view_dispatcher_remove_view(app->view_dispatcher, JsAppViewLoading);
view_dispatcher_free(app->view_dispatcher);
furi_record_close("gui");
free(app);
}
int32_t js_app(void* arg) {
JsApp* app = js_app_alloc();
FuriString* script_path = furi_string_alloc_set(APP_ASSETS_PATH());
do {
if(arg != NULL && strlen(arg) > 0) {
furi_string_set(script_path, (const char*)arg);
} else {
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(&browser_options, ".js", &I_js_script_10px);
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
if(!dialog_file_browser_show(dialogs, script_path, script_path, &browser_options))
break;
furi_record_close(RECORD_DIALOGS);
}
FuriString* name = furi_string_alloc();
path_extract_filename(script_path, name, false);
FuriString* start_text =
furi_string_alloc_printf("Running %s", furi_string_get_cstr(name));
console_view_print(app->console_view, furi_string_get_cstr(start_text));
console_view_print(app->console_view, "------------");
furi_string_free(name);
furi_string_free(start_text);
app->js_thread = js_thread_run(furi_string_get_cstr(script_path), js_callback, app);
view_dispatcher_run(app->view_dispatcher);
js_thread_stop(app->js_thread);
} while(0);
furi_string_free(script_path);
js_app_free(app);
return 0;
} //-V773

View File

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

View File

@@ -0,0 +1,126 @@
#include <core/common_defines.h>
#include "js_modules.h"
#include <m-dict.h>
#include "modules/js_flipper.h"
#define TAG "JS modules"
typedef struct {
JsModeConstructor create;
JsModeDestructor destroy;
void* context;
} JsModuleData;
DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST);
static const JsModuleDescriptor modules_builtin[] = {
{"flipper", js_flipper_create, NULL},
};
struct JsModules {
struct mjs* mjs;
JsModuleDict_t module_dict;
PluginManager* plugin_manager;
};
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) {
JsModules* modules = malloc(sizeof(JsModules));
modules->mjs = mjs;
JsModuleDict_init(modules->module_dict);
modules->plugin_manager = plugin_manager_alloc(
PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver));
return modules;
}
void js_modules_destroy(JsModules* modules) {
JsModuleDict_it_t it;
for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it);
JsModuleDict_next(it)) {
const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it);
if(module_itref->value.destroy) {
module_itref->value.destroy(module_itref->value.context);
}
}
plugin_manager_free(modules->plugin_manager);
JsModuleDict_clear(modules->module_dict);
free(modules);
}
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
JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name);
if(module_inst) { //-V547
furi_string_free(module_name);
mjs_prepend_errorf(
modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name);
return MJS_UNDEFINED;
}
bool module_found = false;
// Check built-in modules
for(size_t i = 0; i < COUNT_OF(modules_builtin); i++) { //-V1008
size_t name_compare_len = strlen(modules_builtin[i].name);
if(name_compare_len != name_len) {
continue;
}
if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) {
JsModuleData module = {
.create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy};
JsModuleDict_set_at(modules->module_dict, module_name, module);
module_found = true;
FURI_LOG_I(TAG, "Using built-in module %s", name);
break;
}
}
// External module load
if(!module_found) {
FuriString* module_path = furi_string_alloc();
furi_string_printf(module_path, "%s/js_%s.fal", APP_DATA_PATH("plugins"), name);
FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path));
do {
uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager);
PluginManagerError load_error = plugin_manager_load_single(
modules->plugin_manager, furi_string_get_cstr(module_path));
if(load_error != PluginManagerErrorNone) {
break;
}
const JsModuleDescriptor* plugin =
plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last);
furi_assert(plugin);
if(strncmp(name, plugin->name, name_len) != 0) {
FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name);
break;
}
JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy};
JsModuleDict_set_at(modules->module_dict, module_name, module);
module_found = true;
} while(0);
furi_string_free(module_path);
}
// Run module constructor
mjs_val_t module_object = MJS_UNDEFINED;
if(module_found) {
module_inst = JsModuleDict_get(modules->module_dict, module_name);
furi_assert(module_inst);
if(module_inst->create) { //-V779
module_inst->context = module_inst->create(modules->mjs, &module_object);
}
}
if(module_object == MJS_UNDEFINED) { //-V547
mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name);
}
furi_string_free(module_name);
return module_object;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "js_thread_i.h"
#include <flipper_application/flipper_application.h>
#include <flipper_application/plugins/plugin_manager.h>
#include <flipper_application/plugins/composite_resolver.h>
#define PLUGIN_APP_ID "js"
#define PLUGIN_API_VERSION 1
typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object);
typedef void (*JsModeDestructor)(void* inst);
typedef struct {
char* name;
JsModeConstructor create;
JsModeDestructor destroy;
} JsModuleDescriptor;
typedef struct JsModules JsModules;
JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver);
void js_modules_destroy(JsModules* modules);
mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,410 @@
#include <core/common_defines.h>
#include "../js_modules.h"
#include <furi_hal.h>
typedef struct {
FuriHalUsbHidConfig* hid_cfg;
FuriHalUsbInterface* usb_if_prev;
uint8_t key_hold_cnt;
} JsBadusbInst;
static const struct {
char* name;
uint16_t code;
} key_codes[] = {
{"CTRL", KEY_MOD_LEFT_CTRL},
{"SHIFT", KEY_MOD_LEFT_SHIFT},
{"ALT", KEY_MOD_LEFT_ALT},
{"GUI", KEY_MOD_LEFT_GUI},
{"DOWN", HID_KEYBOARD_DOWN_ARROW},
{"LEFT", HID_KEYBOARD_LEFT_ARROW},
{"RIGHT", HID_KEYBOARD_RIGHT_ARROW},
{"UP", HID_KEYBOARD_UP_ARROW},
{"ENTER", HID_KEYBOARD_RETURN},
{"PAUSE", HID_KEYBOARD_PAUSE},
{"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
{"DELETE", HID_KEYBOARD_DELETE_FORWARD},
{"BACKSPACE", HID_KEYBOARD_DELETE},
{"END", HID_KEYBOARD_END},
{"ESC", HID_KEYBOARD_ESCAPE},
{"HOME", HID_KEYBOARD_HOME},
{"INSERT", HID_KEYBOARD_INSERT},
{"NUMLOCK", HID_KEYPAD_NUMLOCK},
{"PAGEUP", HID_KEYBOARD_PAGE_UP},
{"PAGEDOWN", HID_KEYBOARD_PAGE_DOWN},
{"PRINTSCREEN", HID_KEYBOARD_PRINT_SCREEN},
{"SCROLLLOCK", HID_KEYBOARD_SCROLL_LOCK},
{"SPACE", HID_KEYBOARD_SPACEBAR},
{"TAB", HID_KEYBOARD_TAB},
{"MENU", HID_KEYBOARD_APPLICATION},
{"F1", HID_KEYBOARD_F1},
{"F2", HID_KEYBOARD_F2},
{"F3", HID_KEYBOARD_F3},
{"F4", HID_KEYBOARD_F4},
{"F5", HID_KEYBOARD_F5},
{"F6", HID_KEYBOARD_F6},
{"F7", HID_KEYBOARD_F7},
{"F8", HID_KEYBOARD_F8},
{"F9", HID_KEYBOARD_F9},
{"F10", HID_KEYBOARD_F10},
{"F11", HID_KEYBOARD_F11},
{"F12", HID_KEYBOARD_F12},
};
static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConfig* hid_cfg) {
if(!mjs_is_object(arg)) {
return false;
}
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0);
mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0);
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
hid_cfg->pid = mjs_get_int32(mjs, pid_obj);
} else {
return false;
}
if(mjs_is_string(mfr_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &mfr_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
strlcpy(hid_cfg->manuf, str_temp, sizeof(hid_cfg->manuf));
}
if(mjs_is_string(prod_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &prod_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product));
}
return true;
}
static void js_badusb_setup(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is already started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
size_t num_args = mjs_nargs(mjs);
if(num_args == 0) {
// No arguments: start USB HID with default settings
args_correct = true;
} else if(num_args == 1) {
badusb->hid_cfg = malloc(sizeof(FuriHalUsbHidConfig));
// Parse argument object
args_correct = setup_parse_params(mjs, mjs_arg(mjs, 0), badusb->hid_cfg);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
badusb->usb_if_prev = furi_hal_usb_get_config();
if(!furi_hal_usb_set_config(&usb_hid, badusb->hid_cfg)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "USB is locked, close companion app first");
badusb->usb_if_prev = NULL;
mjs_return(mjs, MJS_UNDEFINED);
return;
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_is_connected(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool is_connected = furi_hal_hid_is_connected();
mjs_return(mjs, mjs_mk_boolean(mjs, is_connected));
}
uint16_t get_keycode_by_name(const char* key_name, size_t name_len) {
if(name_len == 1) { // Single char
return (HID_ASCII_TO_KEY(key_name[0]));
}
for(size_t i = 0; i < COUNT_OF(key_codes); i++) {
size_t key_cmd_len = strlen(key_codes[i].name);
if(key_cmd_len != name_len) {
continue;
}
if(strncmp(key_name, key_codes[i].name, name_len) == 0) {
return key_codes[i].code;
}
}
return HID_KEYBOARD_NONE;
}
static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) {
uint16_t key_tmp = 0;
for(size_t i = 0; i < nargs; i++) {
mjs_val_t arg = mjs_arg(mjs, i);
if(mjs_is_string(arg)) {
size_t name_len = 0;
const char* key_name = mjs_get_string(mjs, &arg, &name_len);
if((key_name == NULL) || (name_len == 0)) {
// String error
return false;
}
uint16_t str_key = get_keycode_by_name(key_name, name_len);
if(str_key == HID_KEYBOARD_NONE) {
// Unknown key code
return false;
}
if((str_key & 0xFF) && (key_tmp & 0xFF)) {
// Main key is already defined
return false;
}
key_tmp |= str_key;
} else if(mjs_is_number(arg)) {
uint32_t keycode_number = (uint32_t)mjs_get_int32(mjs, arg);
if(((key_tmp & 0xFF) != 0) || (keycode_number > 0xFF)) {
return false;
}
key_tmp |= keycode_number & 0xFF;
} else {
return false;
}
}
*keycode = key_tmp;
return true;
}
static void js_badusb_press(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args > 0) {
args_correct = parse_keycode(mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_hold(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args > 0) {
args_correct = parse_keycode(mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if(keycode & 0xFF) {
badusb->key_hold_cnt++;
if(badusb->key_hold_cnt > (HID_KB_MAX_KEYS - 1)) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Too many keys are hold");
furi_hal_hid_kb_release_all();
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
furi_hal_hid_kb_press(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_release(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint16_t keycode = HID_KEYBOARD_NONE;
size_t num_args = mjs_nargs(mjs);
if(num_args == 0) {
furi_hal_hid_kb_release_all();
badusb->key_hold_cnt = 0;
mjs_return(mjs, MJS_UNDEFINED);
return;
} else {
args_correct = parse_keycode(mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
if((keycode & 0xFF) && (badusb->key_hold_cnt > 0)) {
badusb->key_hold_cnt--;
}
furi_hal_hid_kb_release(keycode);
mjs_return(mjs, MJS_UNDEFINED);
}
static void badusb_print(struct mjs* mjs, bool ln) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
const char* text_str = NULL;
size_t text_len = 0;
uint32_t delay_val = 0;
do {
mjs_val_t obj_string = MJS_UNDEFINED;
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
obj_string = mjs_arg(mjs, 0);
} else if(num_args == 2) {
obj_string = mjs_arg(mjs, 0);
mjs_val_t obj_delay = mjs_arg(mjs, 1);
if(!mjs_is_number(obj_delay)) {
break;
}
delay_val = (uint32_t)mjs_get_int32(mjs, obj_delay);
if(delay_val > 60000) {
break;
}
}
if(!mjs_is_string(obj_string)) {
break;
}
text_str = mjs_get_string(mjs, &obj_string, &text_len);
if((text_str == NULL) || (text_len == 0)) {
break;
}
args_correct = true;
} while(0);
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
for(size_t i = 0; i < text_len; i++) {
uint16_t keycode = HID_ASCII_TO_KEY(text_str[i]);
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
if(delay_val > 0) {
bool need_exit = js_delay_with_flags(mjs, delay_val);
if(need_exit) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
}
if(ln) {
furi_hal_hid_kb_press(HID_KEYBOARD_RETURN);
furi_hal_hid_kb_release(HID_KEYBOARD_RETURN);
}
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_print(struct mjs* mjs) {
badusb_print(mjs, false);
}
static void js_badusb_println(struct mjs* mjs) {
badusb_print(mjs, true);
}
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) {
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
mjs_val_t badusb_obj = mjs_mk_object(mjs);
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
mjs_set(mjs, badusb_obj, "setup", ~0, MJS_MK_FN(js_badusb_setup));
mjs_set(mjs, badusb_obj, "isConnected", ~0, MJS_MK_FN(js_badusb_is_connected));
mjs_set(mjs, badusb_obj, "press", ~0, MJS_MK_FN(js_badusb_press));
mjs_set(mjs, badusb_obj, "hold", ~0, MJS_MK_FN(js_badusb_hold));
mjs_set(mjs, badusb_obj, "release", ~0, MJS_MK_FN(js_badusb_release));
mjs_set(mjs, badusb_obj, "print", ~0, MJS_MK_FN(js_badusb_print));
mjs_set(mjs, badusb_obj, "println", ~0, MJS_MK_FN(js_badusb_println));
*object = badusb_obj;
return badusb;
}
static void js_badusb_destroy(void* inst) {
JsBadusbInst* badusb = inst;
if(badusb->usb_if_prev) {
furi_hal_hid_kb_release_all();
furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL));
}
if(badusb->hid_cfg) {
free(badusb->hid_cfg);
}
free(badusb);
}
static const JsModuleDescriptor js_badusb_desc = {
"badusb",
js_badusb_create,
js_badusb_destroy,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_badusb_desc,
};
const FlipperAppPluginDescriptor* js_badusb_ep(void) {
return &plugin_descriptor;
}

View File

@@ -0,0 +1,154 @@
#include <core/common_defines.h>
#include "../js_modules.h"
#include <dialogs/dialogs.h>
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_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));
*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;
}

View File

@@ -0,0 +1,36 @@
#include <core/common_defines.h>
#include "../js_modules.h"
#include <furi_hal_version.h>
#include <power/power_service/power.h>
static void js_flipper_get_model(struct mjs* mjs) {
mjs_val_t ret = mjs_mk_string(mjs, furi_hal_version_get_model_name(), ~0, true);
mjs_return(mjs, ret);
}
static void js_flipper_get_name(struct mjs* mjs) {
const char* name_str = furi_hal_version_get_name_ptr();
if(name_str == NULL) {
name_str = "Unknown";
}
mjs_val_t ret = mjs_mk_string(mjs, name_str, ~0, true);
mjs_return(mjs, ret);
}
static void js_flipper_get_battery(struct mjs* mjs) {
Power* power = furi_record_open(RECORD_POWER);
PowerInfo info;
power_get_info(power, &info);
furi_record_close(RECORD_POWER);
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
}
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object) {
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, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
mjs_set(mjs, flipper_obj, "getBatteryCharge", ~0, MJS_MK_FN(js_flipper_get_battery));
*object = flipper_obj;
return (void*)1;
}

View File

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

View File

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

View File

@@ -0,0 +1,585 @@
#include <core/common_defines.h>
#include <furi_hal.h>
#include "../js_modules.h"
#include <m-array.h>
#define TAG "js_uart"
#define RX_BUF_LEN 2048
typedef struct {
bool setup_done;
FuriStreamBuffer* rx_stream;
FuriHalSerialHandle* serial_handle;
struct mjs* mjs;
} JsUartInst;
typedef struct {
size_t len;
char* data;
} PatternArrayItem;
ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST);
static void
js_uart_on_async_rx(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) {
JsUartInst* uart = context;
furi_assert(uart);
if(event & FuriHalSerialRxEventData) {
uint8_t data = furi_hal_serial_async_rx(handle);
furi_stream_buffer_send(uart->rx_stream, &data, 1, 0);
js_flags_set(uart->mjs, ThreadEventCustomDataRx);
}
}
static void js_uart_setup(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUartInst* uart = mjs_get_ptr(mjs, obj_inst);
furi_assert(uart);
if(uart->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "UART is already configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint32_t baudrate = 0;
if(mjs_nargs(mjs) == 1) {
mjs_val_t arg = mjs_arg(mjs, 0);
if(mjs_is_number(arg)) {
baudrate = mjs_get_int32(mjs, arg);
args_correct = true;
}
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1);
uart->serial_handle = furi_hal_serial_control_acquire(FuriHalSerialIdLpuart);
if(uart->serial_handle) {
furi_hal_serial_init(uart->serial_handle, baudrate);
furi_hal_serial_async_rx_start(uart->serial_handle, js_uart_on_async_rx, uart, false);
uart->setup_done = true;
}
}
static void js_uart_write(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUartInst* uart = mjs_get_ptr(mjs, obj_inst);
furi_assert(uart);
if(!uart->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "UART is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = true;
size_t num_args = mjs_nargs(mjs);
for(size_t i = 0; i < num_args; i++) {
mjs_val_t arg = mjs_arg(mjs, i);
if(mjs_is_string(arg)) {
size_t str_len = 0;
const char* arg_str = mjs_get_string(mjs, &arg, &str_len);
if((str_len == 0) || (arg_str == NULL)) {
args_correct = false;
break;
}
furi_hal_serial_tx(uart->serial_handle, (uint8_t*)arg_str, str_len);
} else if(mjs_is_number(arg)) {
uint32_t byte_val = mjs_get_int32(mjs, arg);
if(byte_val > 0xFF) {
args_correct = false;
break;
}
furi_hal_serial_tx(uart->serial_handle, (uint8_t*)&byte_val, 1);
} else if(mjs_is_array(arg)) {
size_t array_len = mjs_array_length(mjs, arg);
for(size_t i = 0; i < array_len; i++) {
mjs_val_t array_arg = mjs_array_get(mjs, arg, i);
if(!mjs_is_number(array_arg)) {
args_correct = false;
break;
}
uint32_t byte_val = mjs_get_int32(mjs, array_arg);
if(byte_val > 0xFF) {
args_correct = false;
break;
}
furi_hal_serial_tx(uart->serial_handle, (uint8_t*)&byte_val, 1);
}
if(!args_correct) {
break;
}
} else if(mjs_is_typed_array(arg)) {
mjs_val_t array_buf = arg;
if(mjs_is_data_view(arg)) {
array_buf = mjs_dataview_get_buf(mjs, arg);
}
size_t len = 0;
char* buf = mjs_array_buf_get_ptr(mjs, array_buf, &len);
furi_hal_serial_tx(uart->serial_handle, (uint8_t*)buf, len);
} else {
args_correct = false;
break;
}
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
}
mjs_return(mjs, MJS_UNDEFINED);
}
static size_t js_uart_receive(JsUartInst* uart, char* buf, size_t len, uint32_t timeout) {
size_t bytes_read = 0;
while(1) {
uint32_t flags = ThreadEventCustomDataRx;
if(furi_stream_buffer_is_empty(uart->rx_stream)) {
flags = js_flags_wait(uart->mjs, ThreadEventCustomDataRx, timeout);
}
if(flags == 0) { // Timeout
break;
} else if(flags & ThreadEventStop) { // Exit flag
bytes_read = 0;
break;
} else if(flags & ThreadEventCustomDataRx) { // New data received
size_t rx_len =
furi_stream_buffer_receive(uart->rx_stream, &buf[bytes_read], len - bytes_read, 0);
bytes_read += rx_len;
if(bytes_read == len) {
break;
}
}
}
return bytes_read;
}
static void js_uart_read(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUartInst* uart = mjs_get_ptr(mjs, obj_inst);
furi_assert(uart);
if(!uart->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "UART is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
size_t read_len = 0;
uint32_t timeout = FuriWaitForever;
do {
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
mjs_val_t arg = mjs_arg(mjs, 0);
if(!mjs_is_number(arg)) {
break;
}
read_len = mjs_get_int32(mjs, arg);
} else if(num_args == 2) {
mjs_val_t len_arg = mjs_arg(mjs, 0);
mjs_val_t timeout_arg = mjs_arg(mjs, 1);
if((!mjs_is_number(len_arg)) || (!mjs_is_number(timeout_arg))) {
break;
}
read_len = mjs_get_int32(mjs, len_arg);
timeout = mjs_get_int32(mjs, timeout_arg);
}
} while(0);
if(read_len == 0) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
char* read_buf = malloc(read_len);
size_t bytes_read = js_uart_receive(uart, read_buf, read_len, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0) {
return_obj = mjs_mk_string(mjs, read_buf, bytes_read, true);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static void js_uart_readln(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUartInst* uart = mjs_get_ptr(mjs, obj_inst);
furi_assert(uart);
if(!uart->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "UART is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
bool args_correct = false;
uint32_t timeout = FuriWaitForever;
do {
size_t num_args = mjs_nargs(mjs);
if(num_args > 1) {
break;
} else if(num_args == 1) {
mjs_val_t arg = mjs_arg(mjs, 0);
if(!mjs_is_number(arg)) {
break;
}
timeout = mjs_get_int32(mjs, arg);
}
args_correct = true;
} while(0);
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
FuriString* rx_buf = furi_string_alloc();
size_t bytes_read = 0;
char read_char = 0;
while(1) {
size_t read_len = js_uart_receive(uart, &read_char, 1, timeout);
if(read_len != 1) {
break;
}
if((read_char == '\r') || (read_char == '\n')) {
break;
} else {
furi_string_push_back(rx_buf, read_char);
bytes_read++;
}
}
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0) {
return_obj = mjs_mk_string(mjs, furi_string_get_cstr(rx_buf), bytes_read, true);
}
mjs_return(mjs, return_obj);
furi_string_free(rx_buf);
}
static void js_uart_read_bytes(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUartInst* uart = mjs_get_ptr(mjs, obj_inst);
furi_assert(uart);
if(!uart->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "UART is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
size_t read_len = 0;
uint32_t timeout = FuriWaitForever;
do {
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
mjs_val_t arg = mjs_arg(mjs, 0);
if(!mjs_is_number(arg)) {
break;
}
read_len = mjs_get_int32(mjs, arg);
} else if(num_args == 2) {
mjs_val_t len_arg = mjs_arg(mjs, 0);
mjs_val_t timeout_arg = mjs_arg(mjs, 1);
if((!mjs_is_number(len_arg)) || (!mjs_is_number(timeout_arg))) {
break;
}
read_len = mjs_get_int32(mjs, len_arg);
timeout = mjs_get_int32(mjs, timeout_arg);
}
} while(0);
if(read_len == 0) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
char* read_buf = malloc(read_len);
size_t bytes_read = js_uart_receive(uart, read_buf, read_len, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0) {
return_obj = mjs_mk_array_buf(mjs, read_buf, bytes_read);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static bool js_uart_expect_parse_string(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) {
size_t str_len = 0;
const char* arg_str = mjs_get_string(mjs, &arg, &str_len);
if((str_len == 0) || (arg_str == NULL)) {
return false;
}
PatternArrayItem* item = PatternArray_push_new(patterns);
item->data = malloc(str_len + 1);
memcpy(item->data, arg_str, str_len);
item->len = str_len;
return true;
}
static bool js_uart_expect_parse_array(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) {
size_t array_len = mjs_array_length(mjs, arg);
if(array_len == 0) {
return false;
}
char* array_data = malloc(array_len + 1);
for(size_t i = 0; i < array_len; i++) {
mjs_val_t array_arg = mjs_array_get(mjs, arg, i);
if(!mjs_is_number(array_arg)) {
free(array_data);
return false;
}
uint32_t byte_val = mjs_get_int32(mjs, array_arg);
if(byte_val > 0xFF) {
free(array_data);
return false;
}
array_data[i] = byte_val;
}
PatternArrayItem* item = PatternArray_push_new(patterns);
item->data = array_data;
item->len = array_len;
return true;
}
static bool
js_uart_expect_parse_args(struct mjs* mjs, PatternArray_t patterns, uint32_t* timeout) {
size_t num_args = mjs_nargs(mjs);
if(num_args == 2) {
mjs_val_t timeout_arg = mjs_arg(mjs, 1);
if(!mjs_is_number(timeout_arg)) {
return false;
}
*timeout = mjs_get_int32(mjs, timeout_arg);
} else if(num_args != 1) {
return false;
}
mjs_val_t patterns_arg = mjs_arg(mjs, 0);
if(mjs_is_string(patterns_arg)) { // Single string pattern
if(!js_uart_expect_parse_string(mjs, patterns_arg, patterns)) {
return false;
}
} else if(mjs_is_array(patterns_arg)) {
size_t array_len = mjs_array_length(mjs, patterns_arg);
if(array_len == 0) {
return false;
}
mjs_val_t array_arg = mjs_array_get(mjs, patterns_arg, 0);
if(mjs_is_number(array_arg)) { // Binary array pattern
if(!js_uart_expect_parse_array(mjs, patterns_arg, patterns)) {
return false;
}
} else if((mjs_is_string(array_arg)) || (mjs_is_array(array_arg))) { // Multiple patterns
for(size_t i = 0; i < array_len; i++) {
mjs_val_t arg = mjs_array_get(mjs, patterns_arg, i);
if(mjs_is_string(arg)) {
if(!js_uart_expect_parse_string(mjs, arg, patterns)) {
return false;
}
} else if(mjs_is_array(arg)) {
if(!js_uart_expect_parse_array(mjs, arg, patterns)) {
return false;
}
}
}
} else {
return false;
}
} else {
return false;
}
return true;
}
static int32_t
js_uart_expect_check_pattern_start(PatternArray_t patterns, char value, int32_t pattern_last) {
size_t array_len = PatternArray_size(patterns);
if((pattern_last + 1) >= (int32_t)array_len) {
return (-1);
}
for(size_t i = pattern_last + 1; i < array_len; i++) {
if(PatternArray_get(patterns, i)->data[0] == value) {
return i;
}
}
return (-1);
}
static void js_uart_expect(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsUartInst* uart = mjs_get_ptr(mjs, obj_inst);
furi_assert(uart);
if(!uart->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "UART is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
uint32_t timeout = FuriWaitForever;
PatternArray_t patterns;
PatternArray_it_t it;
PatternArray_init(patterns);
if(!js_uart_expect_parse_args(mjs, patterns, &timeout)) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
mjs_return(mjs, MJS_UNDEFINED);
for(PatternArray_it(it, patterns); !PatternArray_end_p(it); PatternArray_next(it)) {
const PatternArrayItem* item = PatternArray_cref(it);
free(item->data);
}
PatternArray_clear(patterns);
return;
}
size_t pattern_len_max = 0;
for(PatternArray_it(it, patterns); !PatternArray_end_p(it); PatternArray_next(it)) {
const PatternArrayItem* item = PatternArray_cref(it);
if(item->len > pattern_len_max) {
pattern_len_max = item->len;
}
}
char* compare_buf = malloc(pattern_len_max);
int32_t pattern_found = -1;
int32_t pattern_candidate = -1;
size_t buf_len = 0;
bool is_timeout = false;
while(1) {
if(buf_len == 0) {
// Empty buffer - read by 1 byte to find pattern start
size_t bytes_read = js_uart_receive(uart, &compare_buf[0], 1, timeout);
if(bytes_read != 1) {
is_timeout = true;
break;
}
pattern_candidate = js_uart_expect_check_pattern_start(patterns, compare_buf[0], -1);
if(pattern_candidate == -1) {
continue;
}
buf_len = 1;
}
assert(pattern_candidate >= 0);
// Read next and try to find pattern match
PatternArrayItem* pattern_cur = PatternArray_get(patterns, pattern_candidate);
pattern_found = pattern_candidate;
for(size_t i = 0; i < pattern_cur->len; i++) {
if(i >= buf_len) {
size_t bytes_read = js_uart_receive(uart, &compare_buf[i], 1, timeout);
if(bytes_read != 1) {
is_timeout = true;
break;
}
buf_len++;
}
if(compare_buf[i] != pattern_cur->data[i]) {
pattern_found = -1;
break;
}
}
if((is_timeout) || (pattern_found >= 0)) {
break;
}
// Search other patterns with the same start char
pattern_candidate =
js_uart_expect_check_pattern_start(patterns, compare_buf[0], pattern_candidate);
if(pattern_candidate >= 0) {
continue;
}
// Look for another pattern start
for(size_t i = 1; i < buf_len; i++) {
pattern_candidate = js_uart_expect_check_pattern_start(patterns, compare_buf[i], -1);
if(pattern_candidate >= 0) {
memmove(&compare_buf[0], &compare_buf[i], buf_len - i);
buf_len -= i;
break;
}
}
if(pattern_candidate >= 0) {
continue;
}
// Nothing found - reset buffer
buf_len = 0;
}
if(is_timeout) {
FURI_LOG_W(TAG, "Expect: timeout");
}
for(PatternArray_it(it, patterns); !PatternArray_end_p(it); PatternArray_next(it)) {
const PatternArrayItem* item = PatternArray_cref(it);
free(item->data);
}
PatternArray_clear(patterns);
free(compare_buf);
if(pattern_found >= 0) {
mjs_return(mjs, mjs_mk_number(mjs, pattern_found));
} else {
mjs_return(mjs, MJS_UNDEFINED);
}
}
static void* js_uart_create(struct mjs* mjs, mjs_val_t* object) {
JsUartInst* js_uart = malloc(sizeof(JsUartInst));
js_uart->mjs = mjs;
mjs_val_t uart_obj = mjs_mk_object(mjs);
mjs_set(mjs, uart_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_uart));
mjs_set(mjs, uart_obj, "setup", ~0, MJS_MK_FN(js_uart_setup));
mjs_set(mjs, uart_obj, "write", ~0, MJS_MK_FN(js_uart_write));
mjs_set(mjs, uart_obj, "read", ~0, MJS_MK_FN(js_uart_read));
mjs_set(mjs, uart_obj, "readln", ~0, MJS_MK_FN(js_uart_readln));
mjs_set(mjs, uart_obj, "readBytes", ~0, MJS_MK_FN(js_uart_read_bytes));
mjs_set(mjs, uart_obj, "expect", ~0, MJS_MK_FN(js_uart_expect));
*object = uart_obj;
return js_uart;
}
static void js_uart_destroy(void* inst) {
JsUartInst* js_uart = inst;
if(js_uart->setup_done) {
furi_hal_serial_async_rx_stop(js_uart->serial_handle);
furi_hal_serial_deinit(js_uart->serial_handle);
furi_hal_serial_control_release(js_uart->serial_handle);
js_uart->serial_handle = NULL;
}
furi_stream_buffer_free(js_uart->rx_stream);
free(js_uart);
}
static const JsModuleDescriptor js_uart_desc = {
"uart",
js_uart_create,
js_uart_destroy,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_uart_desc,
};
const FlipperAppPluginDescriptor* js_uart_ep(void) {
return &plugin_descriptor;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include <flipper_application/api_hashtable/api_hashtable.h>
/*
* Resolver interface with private application's symbols.
* Implementation is contained in app_api_table.c
*/
extern const ElfApiInterface* const application_api_interface;

View File

@@ -0,0 +1,27 @@
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/api_hashtable/compilesort.hpp>
/*
* This file contains an implementation of a symbol table
* with private app's symbols. It is used by composite API resolver
* to load plugins that use internal application's APIs.
*/
#include "app_api_table_i.h"
static_assert(!has_hash_collisions(app_api_table), "Detected API method hash collision!");
constexpr HashtableApiInterface applicaton_hashtable_api_interface{
{
.api_version_major = 0,
.api_version_minor = 0,
/* generic resolver using pre-sorted array */
.resolver_callback = &elf_resolve_from_hashtable,
},
/* pointers to application's API table boundaries */
.table_cbegin = app_api_table.cbegin(),
.table_cend = app_api_table.cend(),
};
/* Casting to generic resolver to use in Composite API resolver */
extern "C" const ElfApiInterface* const application_api_interface =
&applicaton_hashtable_api_interface;

View File

@@ -0,0 +1,11 @@
#include <assets_icons.h>
#include "js_plugin_api.h"
/*
* A list of app's private functions and objects to expose for plugins.
* It is used to generate a table of symbols for import resolver to use.
* TBD: automatically generate this table from app's header files
*/
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_flags_set, void, (struct mjs*, uint32_t)),
API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t))));

View File

@@ -0,0 +1,18 @@
#pragma once
#include <furi.h>
#include <mjs_core_public.h>
#ifdef __cplusplus
extern "C" {
#endif
bool js_delay_with_flags(struct mjs* mjs, uint32_t time);
void js_flags_set(struct mjs* mjs, uint32_t flags);
uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,43 @@
#pragma once
#include <stdint.h>
/*
Fontname: -misc-spleen-medium-r-normal--8-80-72-72-C-50-ISO10646-1
Copyright: Copyright (c) 2018-2022, Frederic Cambus
Glyphs: 96/472
BBX Build Mode: 2
*/
static const uint8_t u8g2_font_spleen5x8_mr[] =
"`\2\3\2\3\4\1\1\4\5\10\0\377\6\377\7\377\1\77\2\217\3\325 \6\305\372\274\2!\10\305"
"Zaw(\7\42\12\305:\245$JrV\0#\15\305\332I\62(\245$\31\224\62\0$\13\305Z"
"\331R\23\65e\214\0%\15\305zI\224\24\263\60)%!\0&\16\305ZY\22%\221\224$R\244"
"\244\0'\7\305Za\235\31(\10\305z\215\255\25\0)\10\305:i\261\255\6*\13\305\372X\24I"
"C$\225\1+\12\305\372h\30\15R\230\3,\10\305\372\314a\226\1-\10\305\372\344!'\1.\7"
"\305\372\34s\0/\13\305za\26fa\26\206\0\60\12\305\332R%\261\224\42\35\61\10\305\372\231\330"
"\66\3\62\12\305\332R\61\222\302!\6\63\12\305\332R-M\242H\7\64\14\305\272a\22%\321\220\205"
"\71\0\65\12\305\272C\22\256a\262\3\66\12\305\332R\70U\242H\7\67\13\305\272C\22\205Y\61G"
"\0\70\12\305\332RI\252D\221\16\71\12\305\332R%\212\306H\7:\10\305\372\264\34\317\1;\11\305"
"\372\264\34\12\263\14<\11\305\372HVL\313\0=\11\305\372\224!\36r\20>\11\305\332i\61\253#"
"\0\77\12\305:R\61\253C\71\2@\13\305\332R%Q\22%\235\1A\14\305\332R%J\206$J"
"\242\30B\12\305\272Se\252D\311\16C\10\305\332K\330:\3D\14\305\272S%J\242$Jv\0"
"E\11\305\332K\70\205\351\14F\12\305\332K\30Na\16\1G\14\305\332K\230(Q\22E\63\0H"
"\16\305\272Q\22%C\22%Q\22\305\0I\10\305\332[\330\66\3J\11\305\332[\330\244#\0K\14"
"\305\272Q\22%S%J\242\30L\7\305\272a\327\31M\16\305\272Q\62$C\22%Q\22\305\0N"
"\15\305\272Q\242$JEI\224(\6O\14\305\332R%J\242$\212t\0P\13\305\272S%J\246"
"\60\207\0Q\14\305\332R%J\242$\212D\5R\13\305\272S%J\246J\24\3S\11\305\332K\252"
"\206\311\16T\10\305\272\203\24v\7U\15\305\272Q\22%Q\22%Q\64\3V\14\305\272Q\22%Q"
"\22E\232\16W\16\305\272Q\22%Q\62$C\22\305\0X\14\305\272Q\22E\232T\211b\0Y\14"
"\305\272Q\22%Q\64&;\0Z\12\305\272C\230\65\16\61\0[\10\305:S\330\343\2\134\13\305\32"
"a\32\246a\32&\0]\10\305:c\237\26\0^\11\305\372YR\313\311\0_\7\305\372\334\207\4`"
"\7\305:i\316\21a\12\305\372\240\32-Q\64\3b\14\305\32a\70U\242$Jv\0c\11\305\372"
"\340\22Vg\0d\14\305za\264DI\224D\321\14e\13\305\372\340\22%C\222\316\0f\12\305Z"
"R\230ma\35\1g\14\305\372\340\22%Q\244&\23\0h\14\305\32a\70U\242$J\242\30i\11"
"\305\372\71\42\26e\0j\11\305\372\71\24\66i\0k\13\305\32a))iIT\6l\10\305:a"
"\257\62\0m\15\305\372X\224\14\311\220DI\24\3n\14\305\372\330T\211\222(\211b\0o\13\305\372"
"\240T\211\222(\322\1p\13\305\372\330T\211\222)\14\1q\13\305\372\340\22%Q\64V\0r\12\305"
"\372\340\22%a\35\2s\11\305\372\340\222\252\311\16t\11\305:a\266\205U\31u\14\305\372X\224D"
"I\224D\321\14v\14\305\372X\224DI\24i:\0w\15\305\372X\224D\311\220\14I\24\3x\13"
"\305\372X\24iR%\212\1y\14\305\372X\224DI\24\215\311\4z\12\305\372\330\20f\265!\6{"
"\12\305ZR\230\31\253\12\0|\7\305Za\77\1}\13\305\32j\30jZ\30i\0~\11\305\372\244"
"H\321I\0\177\6\305\372\274\2\0\0\0\4\377\377\0";

View File

@@ -0,0 +1,164 @@
#include "../js_app_i.h"
#include "console_font.h"
#define CONSOLE_LINES 8
#define CONSOLE_CHAR_W 5
#define LINE_BREAKS_MAX 3
#define LINE_LEN_MAX (128 / CONSOLE_CHAR_W)
struct JsConsoleView {
View* view;
};
typedef struct {
FuriString* text[CONSOLE_LINES];
} JsConsoleViewModel;
static void console_view_draw_callback(Canvas* canvas, void* _model) {
JsConsoleViewModel* model = _model;
canvas_set_color(canvas, ColorBlack);
canvas_set_custom_u8g2_font(canvas, u8g2_font_spleen5x8_mr);
uint8_t line_h = canvas_current_font_height(canvas);
for(size_t i = 0; i < CONSOLE_LINES; i++) {
canvas_draw_str(canvas, 0, (i + 1) * line_h - 1, furi_string_get_cstr(model->text[i]));
if(furi_string_size(model->text[i]) > LINE_LEN_MAX) {
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 128 - 7, (i + 1) * line_h - 1, "...");
canvas_set_custom_u8g2_font(canvas, u8g2_font_spleen5x8_mr);
}
}
}
static bool console_view_input_callback(InputEvent* event, void* context) {
UNUSED(event);
UNUSED(context);
return false;
}
void console_view_push_line(JsConsoleView* console_view, const char* text, bool line_trimmed) {
with_view_model(
console_view->view,
JsConsoleViewModel * model,
{
FuriString* str_temp = model->text[0];
for(size_t i = 0; i < CONSOLE_LINES - 1; i++) {
model->text[i] = model->text[i + 1];
}
if(!line_trimmed) {
furi_string_printf(str_temp, "%.*s", LINE_LEN_MAX, text);
} else {
// Leave some space for dots
furi_string_printf(str_temp, "%.*s ", LINE_LEN_MAX - 1, text);
}
model->text[CONSOLE_LINES - 1] = str_temp;
},
true);
}
void console_view_print(JsConsoleView* console_view, const char* text) {
char line_buf[LINE_LEN_MAX + 1];
uint8_t line_buf_cnt = 0;
uint8_t utf8_bytes_left = 0;
uint8_t line_break_cnt = 0;
bool line_trim = false;
for(size_t i = 0; i < strlen(text); i++) {
if(text[i] & 0x80) { // UTF8 or another non-ascii character byte
if(utf8_bytes_left > 0) {
utf8_bytes_left--;
if(utf8_bytes_left == 0) {
line_buf[line_buf_cnt++] = '?';
}
} else {
if((text[i] & 0xE0) == 0xC0) {
utf8_bytes_left = 1;
} else if((text[i] & 0xF0) == 0xE0) {
utf8_bytes_left = 2;
} else if((text[i] & 0xF8) == 0xF0) {
utf8_bytes_left = 3;
} else {
line_buf[line_buf_cnt++] = '?';
}
}
} else {
if(utf8_bytes_left > 0) {
utf8_bytes_left = 0;
line_buf[line_buf_cnt++] = '?';
if(line_buf_cnt >= LINE_LEN_MAX) {
line_break_cnt++;
if(line_break_cnt >= LINE_BREAKS_MAX) {
line_trim = true;
break;
}
line_buf[line_buf_cnt] = '\0';
console_view_push_line(console_view, line_buf, false);
line_buf_cnt = 1;
line_buf[0] = ' ';
}
}
if(text[i] == '\n') {
line_buf[line_buf_cnt] = '\0';
line_buf_cnt = 0;
console_view_push_line(console_view, line_buf, false);
} else {
line_buf[line_buf_cnt++] = text[i];
}
if(line_buf_cnt >= LINE_LEN_MAX) {
line_break_cnt++;
if(line_break_cnt >= LINE_BREAKS_MAX) {
line_trim = true;
break;
}
line_buf[line_buf_cnt] = '\0';
console_view_push_line(console_view, line_buf, false);
line_buf_cnt = 1;
line_buf[0] = ' ';
}
}
}
if(line_buf_cnt > 0) {
line_buf[line_buf_cnt] = '\0';
console_view_push_line(console_view, line_buf, line_trim);
}
}
JsConsoleView* console_view_alloc(void) {
JsConsoleView* console_view = malloc(sizeof(JsConsoleView));
console_view->view = view_alloc();
view_set_draw_callback(console_view->view, console_view_draw_callback);
view_set_input_callback(console_view->view, console_view_input_callback);
view_allocate_model(console_view->view, ViewModelTypeLocking, sizeof(JsConsoleViewModel));
with_view_model(
console_view->view,
JsConsoleViewModel * model,
{
for(size_t i = 0; i < CONSOLE_LINES; i++) {
model->text[i] = furi_string_alloc();
}
},
true);
return console_view;
}
void console_view_free(JsConsoleView* console_view) {
with_view_model(
console_view->view,
JsConsoleViewModel * model,
{
for(size_t i = 0; i < CONSOLE_LINES; i++) {
furi_string_free(model->text[i]);
}
},
false);
view_free(console_view->view);
free(console_view);
}
View* console_view_get_view(JsConsoleView* console_view) {
return console_view->view;
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include <gui/view.h>
typedef struct JsConsoleView JsConsoleView;
JsConsoleView* console_view_alloc(void);
void console_view_free(JsConsoleView* console_view);
View* console_view_get_view(JsConsoleView* console_view);
void console_view_print(JsConsoleView* console_view, const char* text);