JS: Backport³ and more additions & fixes (#3961)

* JS: Fix file select for fbt launch js_app
* JS: badusb: Add numpad keys
  Co-authored-by: oldip <oldip@users.noreply.github.com>
* JS: badusb: Layout support
* JS: badusb: altPrint() and altPrintln()
  Co-authored-by: oldip <oldip@users.noreply.github.com>
* JS: badusb: quit()
* JS: serial: readAny()
* JS: serial: end()
* JS: serial: Auto disable expansion service
* JS: storage: Add example script
* JS: gui: text_input: Fix NULL ptr when no prop given
* JS: gui: text_input: Default text props
  Co-authored-by: xMasterX <xMasterX@users.noreply.github.com>
* JS: gui: byte_input
  Co-authored-by: xMasterX <xMasterX@users.noreply.github.com>
* JS: gui: file_picker
* JS: gui: viewDispatcher.currentView
* JS: gui: view.hasProperty()
* JS: gui: Add some missing typedefs comments
* JS: globals: Fix toString() with negative numbers
* JS: globals: parseInt()
  Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
* JS: globals: toUpperCase() and toLowerCase()
  Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
* JS: globals: Add some missing typedefs
* JS: Add example for string functions
  Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
* JS: globals: __dirpath and __filepath
  Co-authored-by: jamisonderek <jamisonderek@users.noreply.github.com>
* JS: globals: load() typedef missing scope param
* JS: Add interactive REPL script example
* JS: Add missing icon for file picker
* JS: Rename to __filename and __dirname
* JS: Move toUpperCase() and toLowerCase() to string class
* JS: parseInt() refactor
* JS: Typedef base param for parseInt()
* Revert "JS: gui: view.hasProperty()"
  This reverts commit 1967ec06d4f2e9cafc4e18384ad370f7a7c44468.
* JS: Move toString() to Number class
* JS: Fix duplicate plugin files
  in plugins, `requires` is used to determine which app to distribute the .fal under `apps_data/appid/plugins`
* JS: math: Missing typedefs, use camelCase
* JS: badusb: layoutPath is optional in typedef
* Fix ASS scoping
* Rename mjs term prop type value
* Change load() description
* Enlarge buffers in default prop assign
* More checks for default data/text size
* Make PVS happy
* Fix icon symbol
* Update types for JS SDK
* toString() was moved to number class

Co-authored-by: oldip <oldip@users.noreply.github.com>
Co-authored-by: xMasterX <xMasterX@users.noreply.github.com>
Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
Co-authored-by: jamisonderek <jamisonderek@users.noreply.github.com>
Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
WillyJL 2024-10-31 05:22:05 +00:00 committed by GitHub
parent 1907f23e5f
commit c807ffc324
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1049 additions and 74 deletions

View File

@ -41,7 +41,7 @@ App(
appid="js_gui",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_ep",
requires=["js_app", "js_event_loop"],
requires=["js_app"],
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
)
@ -49,7 +49,7 @@ App(
appid="js_gui__loading",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_loading_ep",
requires=["js_app", "js_gui", "js_event_loop"],
requires=["js_app"],
sources=["modules/js_gui/loading.c"],
)
@ -57,7 +57,7 @@ App(
appid="js_gui__empty_screen",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_empty_screen_ep",
requires=["js_app", "js_gui", "js_event_loop"],
requires=["js_app"],
sources=["modules/js_gui/empty_screen.c"],
)
@ -65,7 +65,7 @@ App(
appid="js_gui__submenu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_submenu_ep",
requires=["js_app", "js_gui"],
requires=["js_app"],
sources=["modules/js_gui/submenu.c"],
)
@ -73,10 +73,18 @@ App(
appid="js_gui__text_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_text_input_ep",
requires=["js_app", "js_gui", "js_event_loop"],
requires=["js_app"],
sources=["modules/js_gui/text_input.c"],
)
App(
appid="js_gui__byte_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_byte_input_ep",
requires=["js_app"],
sources=["modules/js_gui/byte_input.c"],
)
App(
appid="js_gui__text_box",
apptype=FlipperAppType.PLUGIN,
@ -93,6 +101,15 @@ App(
sources=["modules/js_gui/dialog.c"],
)
App(
appid="js_gui__file_picker",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gui_file_picker_ep",
requires=["js_app"],
sources=["modules/js_gui/file_picker.c"],
fap_libs=["assets"],
)
App(
appid="js_notification",
apptype=FlipperAppType.PLUGIN,
@ -121,7 +138,7 @@ App(
appid="js_gpio",
apptype=FlipperAppType.PLUGIN,
entry_point="js_gpio_ep",
requires=["js_app", "js_event_loop"],
requires=["js_app"],
sources=["modules/js_gpio.c"],
)

View File

@ -13,7 +13,13 @@ let views = {
}),
};
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" });
badusb.setup({
vid: 0xAAAA,
pid: 0xBBBB,
mfrName: "Flipper",
prodName: "Zero",
layoutPath: "/ext/badusb/assets/layouts/en-US.kl"
});
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
if (button !== "center")
@ -39,7 +45,13 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui)
badusb.println("Flipper Model: " + flipper.getModel());
badusb.println("Flipper Name: " + flipper.getName());
badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%");
badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%");
// Alt+Numpad method works only on Windows!!!
badusb.altPrintln("This was printed with Alt+Numpad method!");
// There's also badusb.print() and badusb.altPrint()
// which don't add the return at the end
notify.success();
} else {
@ -47,6 +59,9 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui)
notify.error();
}
// Optional, but allows to unlock usb interface to switch profile
badusb.quit();
eventLoop.stop();
}, eventLoop, gui);

View File

@ -5,8 +5,11 @@ let loadingView = require("gui/loading");
let submenuView = require("gui/submenu");
let emptyView = require("gui/empty_screen");
let textInputView = require("gui/text_input");
let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker");
let flipper = require("flipper");
// declare view instances
let views = {
@ -16,9 +19,14 @@ let views = {
header: "Enter your name",
minLength: 0,
maxLength: 32,
defaultText: flipper.getName(),
defaultTextClear: true,
}),
helloDialog: dialogView.makeWith({
center: "Hi Flipper! :)",
helloDialog: dialogView.make(),
bytekb: byteInputView.makeWith({
header: "Look ma, I'm a header text!",
length: 8,
defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]),
}),
longText: textBoxView.makeWith({
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
@ -29,7 +37,9 @@ let views = {
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Exit app",
],
}),
@ -49,15 +59,28 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
} else if (index === 2) {
gui.viewDispatcher.switchTo(views.keyboard);
} else if (index === 3) {
gui.viewDispatcher.switchTo(views.longText);
gui.viewDispatcher.switchTo(views.bytekb);
} else if (index === 4) {
gui.viewDispatcher.switchTo(views.longText);
} else if (index === 5) {
let path = filePicker.pickFile("/ext", "*");
if (path) {
views.helloDialog.set("text", "You selected:\n" + path);
} else {
views.helloDialog.set("text", "You didn't select a file");
}
views.helloDialog.set("center", "Nice!");
gui.viewDispatcher.switchTo(views.helloDialog);
} else if (index === 6) {
eventLoop.stop();
}
}, gui, eventLoop, views);
// say hi after keyboard input
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
views.keyboard.set("defaultText", name); // Remember for next usage
views.helloDialog.set("text", "Hi " + name + "! :)");
views.helloDialog.set("center", "Hi Flipper! :)");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
@ -67,11 +90,27 @@ eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views)
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// go to the demo chooser screen when the back key is pressed
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
// show data after byte input
eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
let data_view = Uint8Array(data);
let text = "0x";
for (let i = 0; i < data_view.length; i++) {
text += data_view[i].toString(16);
}
views.helloDialog.set("text", "You typed:\n" + text);
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// go to the demo chooser screen when the back key is pressed
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views, eventLoop) {
if (gui.viewDispatcher.currentView === views.demos) {
eventLoop.stop();
return;
}
gui.viewDispatcher.switchTo(views.demos);
}, gui, views, eventLoop);
// run UI
gui.viewDispatcher.switchTo(views.demos);
eventLoop.run();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -97,7 +97,7 @@ static void js_app_free(JsApp* app) {
int32_t js_app(void* arg) {
JsApp* app = js_app_alloc();
FuriString* script_path = furi_string_alloc_set(APP_ASSETS_PATH());
FuriString* script_path = furi_string_alloc_set(EXT_PATH("apps/Scripts"));
do {
if(arg != NULL && strlen(arg) > 0) {
furi_string_set(script_path, (const char*)arg);

View File

@ -1,5 +1,7 @@
#include <common/cs_dbg.h>
#include <toolbox/path.h>
#include <toolbox/stream/file_stream.h>
#include <toolbox/strint.h>
#include <loader/firmware_api/firmware_api.h>
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/plugins/composite_resolver.h>
@ -194,6 +196,27 @@ static void js_require(struct mjs* mjs) {
mjs_return(mjs, req_object);
}
static void js_parse_int(struct mjs* mjs) {
const char* str;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str));
int32_t base = 10;
if(mjs_nargs(mjs) >= 2) {
mjs_val_t base_arg = mjs_arg(mjs, 1);
if(!mjs_is_number(base_arg)) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number");
mjs_return(mjs, MJS_UNDEFINED);
}
base = mjs_get_int(mjs, base_arg);
}
int32_t num;
if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) {
num = 0;
}
mjs_return(mjs, mjs_mk_number(mjs, num));
}
static void js_global_to_string(struct mjs* mjs) {
int base = 10;
if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1));
@ -233,10 +256,30 @@ static int32_t js_thread(void* arg) {
mjs_val_t global = mjs_get_global(mjs);
mjs_val_t console_obj = mjs_mk_object(mjs);
if(worker->path) {
FuriString* dirpath = furi_string_alloc();
path_extract_dirname(furi_string_get_cstr(worker->path), dirpath);
mjs_set(
mjs,
global,
"__filename",
~0,
mjs_mk_string(
mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true));
mjs_set(
mjs,
global,
"__dirname",
~0,
mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true));
furi_string_free(dirpath);
}
JS_ASSIGN_MULTI(mjs, global) {
JS_FIELD("print", MJS_MK_FN(js_print));
JS_FIELD("delay", MJS_MK_FN(js_delay));
JS_FIELD("toString", MJS_MK_FN(js_global_to_string));
JS_FIELD("parseInt", MJS_MK_FN(js_parse_int));
JS_FIELD("ffi_address", MJS_MK_FN(js_ffi_address));
JS_FIELD("require", MJS_MK_FN(js_require));
JS_FIELD("console", console_obj);

View File

@ -2,8 +2,11 @@
#include "../js_modules.h"
#include <furi_hal.h>
#define ASCII_TO_KEY(layout, x) (((uint8_t)x < 128) ? (layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
typedef struct {
FuriHalUsbHidConfig* hid_cfg;
uint16_t layout[128];
FuriHalUsbInterface* usb_if_prev;
uint8_t key_hold_cnt;
} JsBadusbInst;
@ -64,9 +67,36 @@ static const struct {
{"F22", HID_KEYBOARD_F22},
{"F23", HID_KEYBOARD_F23},
{"F24", HID_KEYBOARD_F24},
{"NUM0", HID_KEYPAD_0},
{"NUM1", HID_KEYPAD_1},
{"NUM2", HID_KEYPAD_2},
{"NUM3", HID_KEYPAD_3},
{"NUM4", HID_KEYPAD_4},
{"NUM5", HID_KEYPAD_5},
{"NUM6", HID_KEYPAD_6},
{"NUM7", HID_KEYPAD_7},
{"NUM8", HID_KEYPAD_8},
{"NUM9", HID_KEYPAD_9},
};
static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConfig* hid_cfg) {
static void js_badusb_quit_free(JsBadusbInst* badusb) {
if(badusb->usb_if_prev) {
furi_hal_hid_kb_release_all();
furi_check(furi_hal_usb_set_config(badusb->usb_if_prev, NULL));
badusb->usb_if_prev = NULL;
}
if(badusb->hid_cfg) {
free(badusb->hid_cfg);
badusb->hid_cfg = NULL;
}
}
static bool setup_parse_params(
JsBadusbInst* badusb,
struct mjs* mjs,
mjs_val_t arg,
FuriHalUsbHidConfig* hid_cfg) {
if(!mjs_is_object(arg)) {
return false;
}
@ -74,6 +104,7 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
mjs_val_t layout_obj = mjs_get(mjs, arg, "layoutPath", ~0);
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
@ -100,6 +131,25 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
strlcpy(hid_cfg->product, str_temp, sizeof(hid_cfg->product));
}
if(mjs_is_string(layout_obj)) {
size_t str_len = 0;
const char* str_temp = mjs_get_string(mjs, &layout_obj, &str_len);
if((str_len == 0) || (str_temp == NULL)) {
return false;
}
File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
bool layout_loaded = storage_file_open(file, str_temp, FSAM_READ, FSOM_OPEN_EXISTING) &&
storage_file_read(file, badusb->layout, sizeof(badusb->layout)) ==
sizeof(badusb->layout);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(!layout_loaded) {
return false;
}
} else {
memcpy(badusb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(badusb->layout)));
}
return true;
}
@ -122,7 +172,7 @@ static void js_badusb_setup(struct mjs* mjs) {
} 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);
args_correct = setup_parse_params(badusb, mjs, mjs_arg(mjs, 0), badusb->hid_cfg);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@ -142,6 +192,22 @@ static void js_badusb_setup(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_quit(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
if(badusb->usb_if_prev == NULL) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_badusb_quit_free(badusb);
mjs_return(mjs, MJS_UNDEFINED);
}
static void js_badusb_is_connected(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
@ -157,9 +223,9 @@ static void js_badusb_is_connected(struct mjs* mjs) {
mjs_return(mjs, mjs_mk_boolean(mjs, is_connected));
}
uint16_t get_keycode_by_name(const char* key_name, size_t name_len) {
uint16_t get_keycode_by_name(JsBadusbInst* badusb, const char* key_name, size_t name_len) {
if(name_len == 1) { // Single char
return HID_ASCII_TO_KEY(key_name[0]);
return (ASCII_TO_KEY(badusb->layout, key_name[0]));
}
for(size_t i = 0; i < COUNT_OF(key_codes); i++) {
@ -176,7 +242,7 @@ uint16_t get_keycode_by_name(const char* key_name, size_t name_len) {
return HID_KEYBOARD_NONE;
}
static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) {
static bool parse_keycode(JsBadusbInst* badusb, struct mjs* mjs, size_t nargs, uint16_t* keycode) {
uint16_t key_tmp = 0;
for(size_t i = 0; i < nargs; i++) {
mjs_val_t arg = mjs_arg(mjs, i);
@ -187,7 +253,7 @@ static bool parse_keycode(struct mjs* mjs, size_t nargs, uint16_t* keycode) {
// String error
return false;
}
uint16_t str_key = get_keycode_by_name(key_name, name_len);
uint16_t str_key = get_keycode_by_name(badusb, key_name, name_len);
if(str_key == HID_KEYBOARD_NONE) {
// Unknown key code
return false;
@ -225,7 +291,7 @@ static void js_badusb_press(struct mjs* mjs) {
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);
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@ -251,7 +317,7 @@ static void js_badusb_hold(struct mjs* mjs) {
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);
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@ -290,7 +356,7 @@ static void js_badusb_release(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED);
return;
} else {
args_correct = parse_keycode(mjs, num_args, &keycode);
args_correct = parse_keycode(badusb, mjs, num_args, &keycode);
}
if(!args_correct) {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
@ -304,7 +370,35 @@ static void js_badusb_release(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED);
}
static void badusb_print(struct mjs* mjs, bool ln) {
// Make sure NUMLOCK is enabled for altchar
static void ducky_numlock_on() {
if((furi_hal_hid_get_led_state() & HID_KB_LED_NUM) == 0) {
furi_hal_hid_kb_press(HID_KEYBOARD_LOCK_NUM_LOCK);
furi_hal_hid_kb_release(HID_KEYBOARD_LOCK_NUM_LOCK);
}
}
// Simulate pressing a character using ALT+Numpad ASCII code
static void ducky_altchar(JsBadusbInst* badusb, const char* ascii_code) {
// Hold the ALT key
furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT);
// Press the corresponding numpad key for each digit of the ASCII code
for(size_t i = 0; ascii_code[i] != '\0'; i++) {
char digitChar[5] = {'N', 'U', 'M', ascii_code[i], '\0'}; // Construct the numpad key name
uint16_t numpad_keycode = get_keycode_by_name(badusb, digitChar, strlen(digitChar));
if(numpad_keycode == HID_KEYBOARD_NONE) {
continue; // Skip if keycode not found
}
furi_hal_hid_kb_press(numpad_keycode);
furi_hal_hid_kb_release(numpad_keycode);
}
// Release the ALT key
furi_hal_hid_kb_release(KEY_MOD_LEFT_ALT);
}
static void badusb_print(struct mjs* mjs, bool ln, bool alt) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst);
furi_assert(badusb);
@ -350,10 +444,20 @@ static void badusb_print(struct mjs* mjs, bool ln) {
return;
}
if(alt) {
ducky_numlock_on();
}
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(alt) {
// Convert character to ascii numeric value
char ascii_str[4];
snprintf(ascii_str, sizeof(ascii_str), "%u", (uint8_t)text_str[i]);
ducky_altchar(badusb, ascii_str);
} else {
uint16_t keycode = ASCII_TO_KEY(badusb->layout, text_str[i]);
furi_hal_hid_kb_press(keycode);
furi_hal_hid_kb_release(keycode);
}
if(delay_val > 0) {
bool need_exit = js_delay_with_flags(mjs, delay_val);
if(need_exit) {
@ -371,11 +475,19 @@ static void badusb_print(struct mjs* mjs, bool ln) {
}
static void js_badusb_print(struct mjs* mjs) {
badusb_print(mjs, false);
badusb_print(mjs, false, false);
}
static void js_badusb_println(struct mjs* mjs) {
badusb_print(mjs, true);
badusb_print(mjs, true, false);
}
static void js_badusb_alt_print(struct mjs* mjs) {
badusb_print(mjs, false, true);
}
static void js_badusb_alt_println(struct mjs* mjs) {
badusb_print(mjs, true, true);
}
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
@ -384,25 +496,22 @@ static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod
mjs_val_t badusb_obj = mjs_mk_object(mjs);
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
mjs_set(mjs, badusb_obj, "setup", ~0, MJS_MK_FN(js_badusb_setup));
mjs_set(mjs, badusb_obj, "quit", ~0, MJS_MK_FN(js_badusb_quit));
mjs_set(mjs, badusb_obj, "isConnected", ~0, MJS_MK_FN(js_badusb_is_connected));
mjs_set(mjs, badusb_obj, "press", ~0, MJS_MK_FN(js_badusb_press));
mjs_set(mjs, badusb_obj, "hold", ~0, MJS_MK_FN(js_badusb_hold));
mjs_set(mjs, badusb_obj, "release", ~0, MJS_MK_FN(js_badusb_release));
mjs_set(mjs, badusb_obj, "print", ~0, MJS_MK_FN(js_badusb_print));
mjs_set(mjs, badusb_obj, "println", ~0, MJS_MK_FN(js_badusb_println));
mjs_set(mjs, badusb_obj, "altPrint", ~0, MJS_MK_FN(js_badusb_alt_print));
mjs_set(mjs, badusb_obj, "altPrintln", ~0, MJS_MK_FN(js_badusb_alt_println));
*object = badusb_obj;
return badusb;
}
static void js_badusb_destroy(void* inst) {
JsBadusbInst* badusb = inst;
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);
}
js_badusb_quit_free(badusb);
free(badusb);
}

View File

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

View File

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

View File

@ -101,8 +101,10 @@ 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);
mjs_val_t vd_obj = mjs_get_this(mjs);
JsGui* module = JS_GET_INST(mjs, vd_obj);
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
mjs_set(mjs, vd_obj, "currentView", ~0, view);
}
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
@ -154,6 +156,7 @@ static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* module
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
JS_FIELD("currentView", MJS_NULL);
}
// create API object
@ -213,7 +216,21 @@ static bool
expected_type = "array";
break;
}
c_value = (JsViewPropValue){.array = value};
c_value = (JsViewPropValue){.term = value};
} break;
case JsViewPropTypeTypedArr: {
if(!mjs_is_typed_array(value)) {
expected_type = "typed_array";
break;
}
c_value = (JsViewPropValue){.term = value};
} break;
case JsViewPropTypeBool: {
if(!mjs_is_boolean(value)) {
expected_type = "bool";
break;
}
c_value = (JsViewPropValue){.boolean = mjs_get_bool(mjs, value)};
} break;
}

View File

@ -9,12 +9,15 @@ typedef enum {
JsViewPropTypeString,
JsViewPropTypeNumber,
JsViewPropTypeArr,
JsViewPropTypeTypedArr,
JsViewPropTypeBool,
} JsViewPropType;
typedef union {
const char* string;
int32_t number;
mjs_val_t array;
bool boolean;
mjs_val_t term;
} JsViewPropValue;
/**

View File

@ -33,9 +33,9 @@ static bool
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);
size_t len = mjs_array_length(mjs, value.term);
for(size_t i = 0; i < len; i++) {
mjs_val_t item = mjs_array_get(mjs, value.array, i);
mjs_val_t item = mjs_array_get(mjs, value.term, i);
if(!mjs_is_string(item)) return false;
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
}

View File

@ -8,7 +8,9 @@
typedef struct {
char* buffer;
size_t buffer_size;
size_t default_text_size;
FuriString* header;
bool default_text_clear;
FuriSemaphore* input_semaphore;
JsEventLoopContract contract;
} JsKbdContext;
@ -48,7 +50,14 @@ static bool max_len_assign(
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
context->buffer_size = (size_t)(value.number + 1);
size_t new_buffer_size = value.number + 1;
if(new_buffer_size < context->default_text_size) {
// Avoid confusing parameters from user
mjs_prepend_errorf(
mjs, MJS_BAD_ARGS_ERROR, "maxLength must be larger than defaultText length");
return false;
}
context->buffer_size = new_buffer_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
text_input_set_result_callback(
input,
@ -56,17 +65,63 @@ static bool max_len_assign(
context,
context->buffer,
context->buffer_size,
true);
context->default_text_clear);
return true;
}
static bool default_text_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
UNUSED(input);
if(value.string) {
context->default_text_size = strlen(value.string) + 1;
if(context->buffer_size < context->default_text_size) {
// Ensure buffer is large enough for defaultData
context->buffer_size = context->default_text_size;
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
}
// Also trim excess previous data with strlcpy()
strlcpy(context->buffer, value.string, context->buffer_size); //-V575
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
}
return true;
}
static bool default_text_clear_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
context->default_text_clear = value.boolean;
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
return true;
}
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
UNUSED(input);
JsKbdContext* context = malloc(sizeof(JsKbdContext));
*context = (JsKbdContext){
.buffer_size = DEFAULT_BUF_SZ,
.buffer = malloc(DEFAULT_BUF_SZ),
.header = furi_string_alloc(),
.default_text_clear = false,
.input_semaphore = furi_semaphore_alloc(1, 0),
};
context->contract = (JsEventLoopContract){
@ -80,8 +135,13 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_
.transformer_context = context,
},
};
UNUSED(mjs);
UNUSED(view_obj);
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
context->default_text_clear);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
@ -101,7 +161,7 @@ static const JsViewDescriptor view_descriptor = {
.get_view = (JsViewGetView)text_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.prop_cnt = 5,
.props = {
(JsViewPropDescriptor){
.name = "header",
@ -115,6 +175,14 @@ static const JsViewDescriptor view_descriptor = {
.name = "maxLength",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_len_assign},
(JsViewPropDescriptor){
.name = "defaultText",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)default_text_assign},
(JsViewPropDescriptor){
.name = "defaultTextClear",
.type = JsViewPropTypeBool,
.assign = (JsViewPropAssign)default_text_clear_assign},
}};
JS_GUI_VIEW_DEF(text_input, &view_descriptor);

View File

@ -308,7 +308,7 @@ void js_math_trunc(struct mjs* mjs) {
static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
mjs_val_t math_obj = mjs_mk_object(mjs);
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal));
mjs_set(mjs, math_obj, "isEqual", ~0, MJS_MK_FN(js_math_is_equal));
mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs));
mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos));
mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh));

View File

@ -1,4 +1,5 @@
#include <core/common_defines.h>
#include <expansion/expansion.h>
#include <furi_hal.h>
#include "../js_modules.h"
#include <m-array.h>
@ -89,16 +90,51 @@ static void js_serial_setup(struct mjs* mjs) {
return;
}
serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1);
expansion_disable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
serial->serial_handle = furi_hal_serial_control_acquire(serial_id);
if(serial->serial_handle) {
serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1);
furi_hal_serial_init(serial->serial_handle, baudrate);
furi_hal_serial_async_rx_start(
serial->serial_handle, js_serial_on_async_rx, serial, false);
serial->setup_done = true;
} else {
expansion_enable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
}
}
static void js_serial_deinit(JsSerialInst* js_serial) {
if(js_serial->setup_done) {
furi_hal_serial_async_rx_stop(js_serial->serial_handle);
furi_hal_serial_deinit(js_serial->serial_handle);
furi_hal_serial_control_release(js_serial->serial_handle);
js_serial->serial_handle = NULL;
furi_stream_buffer_free(js_serial->rx_stream);
expansion_enable(furi_record_open(RECORD_EXPANSION));
furi_record_close(RECORD_EXPANSION);
js_serial->setup_done = false;
}
}
static void js_serial_end(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
furi_assert(serial);
if(!serial->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
js_serial_deinit(serial);
}
static void js_serial_write(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
@ -346,6 +382,55 @@ static void js_serial_read_bytes(struct mjs* mjs) {
free(read_buf);
}
static char* js_serial_receive_any(JsSerialInst* serial, size_t* len, uint32_t timeout) {
uint32_t flags = ThreadEventCustomDataRx;
if(furi_stream_buffer_is_empty(serial->rx_stream)) {
flags = js_flags_wait(serial->mjs, ThreadEventCustomDataRx, timeout);
}
if(flags & ThreadEventCustomDataRx) { // New data received
*len = furi_stream_buffer_bytes_available(serial->rx_stream);
if(!*len) return NULL;
char* buf = malloc(*len);
furi_stream_buffer_receive(serial->rx_stream, buf, *len, 0);
return buf;
}
return NULL;
}
static void js_serial_read_any(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst);
furi_assert(serial);
if(!serial->setup_done) {
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured");
mjs_return(mjs, MJS_UNDEFINED);
return;
}
uint32_t timeout = FuriWaitForever;
do {
size_t num_args = mjs_nargs(mjs);
if(num_args == 1) {
mjs_val_t timeout_arg = mjs_arg(mjs, 0);
if(!mjs_is_number(timeout_arg)) {
break;
}
timeout = mjs_get_int32(mjs, timeout_arg);
}
} while(0);
size_t bytes_read = 0;
char* read_buf = js_serial_receive_any(serial, &bytes_read, timeout);
mjs_val_t return_obj = MJS_UNDEFINED;
if(bytes_read > 0 && read_buf) {
return_obj = mjs_mk_string(mjs, read_buf, bytes_read, true);
}
mjs_return(mjs, return_obj);
free(read_buf);
}
static bool
js_serial_expect_parse_string(struct mjs* mjs, mjs_val_t arg, PatternArray_t patterns) {
size_t str_len = 0;
@ -580,10 +665,12 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod
mjs_val_t serial_obj = mjs_mk_object(mjs);
mjs_set(mjs, serial_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_serial));
mjs_set(mjs, serial_obj, "setup", ~0, MJS_MK_FN(js_serial_setup));
mjs_set(mjs, serial_obj, "end", ~0, MJS_MK_FN(js_serial_end));
mjs_set(mjs, serial_obj, "write", ~0, MJS_MK_FN(js_serial_write));
mjs_set(mjs, serial_obj, "read", ~0, MJS_MK_FN(js_serial_read));
mjs_set(mjs, serial_obj, "readln", ~0, MJS_MK_FN(js_serial_readln));
mjs_set(mjs, serial_obj, "readBytes", ~0, MJS_MK_FN(js_serial_read_bytes));
mjs_set(mjs, serial_obj, "readAny", ~0, MJS_MK_FN(js_serial_read_any));
mjs_set(mjs, serial_obj, "expect", ~0, MJS_MK_FN(js_serial_expect));
*object = serial_obj;
@ -592,14 +679,7 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod
static void js_serial_destroy(void* inst) {
JsSerialInst* js_serial = inst;
if(js_serial->setup_done) {
furi_hal_serial_async_rx_stop(js_serial->serial_handle);
furi_hal_serial_deinit(js_serial->serial_handle);
furi_hal_serial_control_release(js_serial->serial_handle);
js_serial->serial_handle = NULL;
}
furi_stream_buffer_free(js_serial->rx_stream);
js_serial_deinit(js_serial);
free(js_serial);
}

View File

@ -16,6 +16,9 @@ export type MainKey =
"F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" |
"F20" | "F21" | "F22" | "F23" | "F24" |
"NUM0" | "NUM1" | "NUM2" | "NUM3" | "NUM4" | "NUM5" | "NUM6" | "NUM7" |
"NUM8" | "NUM9" |
"\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" |
"+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" |
"]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" |
@ -38,7 +41,7 @@ export type KeyCode = MainKey | ModifierKey | number;
* @param settings USB device settings. Omit to select default parameters
* @version Added in JS SDK 0.1
*/
export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void;
export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath?: string }): void;
/**
* @brief Tells whether the virtual USB HID device has successfully connected
@ -89,3 +92,26 @@ export declare function print(string: string, delay?: number): void;
* @version Added in JS SDK 0.1
*/
export declare function println(string: string, delay?: number): void;
/**
* @brief Prints a string by Alt+Numpad method - works only on Windows!
* @param string The string to print
* @param delay How many milliseconds to wait between key presses
* @version Added in JS SDK 0.1
*/
export declare function altPrint(string: string, delay?: number): void;
/**
* @brief Prints a string by Alt+Numpad method - works only on Windows!
* Presses "Enter" after printing the string
* @param string The string to print
* @param delay How many milliseconds to wait between key presses
* @version Added in JS SDK 0.1
*/
export declare function altPrintln(string: string, delay?: number): void;
/**
* @brief Releases usb, optional, but allows to switch usb profile
* @version Added in JS SDK 0.1
*/
export declare function quit(): void;

View File

@ -149,14 +149,6 @@ declare function delay(ms: number): void;
*/
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: 10
* @version Added in JS SDK 0.1
*/
declare function toString(value: number, base?: number): string;
/**
* @brief Reads a JS value from a file
*
@ -327,13 +319,44 @@ declare class String {
* @version Added in JS SDK 0.1
*/
at(index: number): number;
/**
* @brief Return index of first occurrence of substr within the string or `-1` if not found
* @param substr The string to search for
* @param fromIndex The index to start searching from
* @version Added in JS SDK 0.1
*/
indexOf(substr: string, fromIndex?: number): number;
/**
* @brief Return a substring between two indices
* @param start The index to start substring at
* @param end The index to end substring at
* @version Added in JS SDK 0.1
*/
slice(start: number, end?: number): string;
/**
* @brief Return this string transformed to upper case
* @version Added in JS SDK 0.1
*/
toUpperCase(): string;
/**
* @brief Return this string transformed to lower case
* @version Added in JS SDK 0.1
*/
toLowerCase(): string;
}
declare class Boolean { }
declare class Function { }
declare class Number { }
declare class Number {
/**
* @brief Converts this number to a string
* @param base Integer base (`2`...`16`), default: 10
* @version Added in JS SDK 0.1
*/
toString(base?: number): string;
}
declare class Object { }

View File

@ -0,0 +1,41 @@
/**
* Displays a byte input keyboard.
*
* <img src="../images/byte_input.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let byteInputView = require("gui/byte_input");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed at the top of the screen
* - `length`: Length of data to edit
* - `defaultData`: Data to show by default
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
length: number,
defaultData: Uint8Array | ArrayBuffer,
}
declare class ByteInput extends View<Props> {
input: Contract<string>;
}
declare class ByteInputFactory extends ViewFactory<Props, ByteInput> { }
declare const factory: ByteInputFactory;
export = factory;

View File

@ -0,0 +1,7 @@
/**
* @brief Displays a file picker and returns the selected file, or undefined if cancelled
* @param basePath The path to start at
* @param extension The file extension(s) to show (like `.sub`, `.iso|.img`, `*`)
* @version Added in JS SDK 0.1
*/
export declare function pickFile(basePath: string, extension: string): string | undefined;

View File

@ -1,5 +1,5 @@
/**
* Displays a keyboard.
* Displays a text input keyboard.
*
* <img src="../images/text_input.png" width="200" alt="Sample screenshot of the view" />
*
@ -20,6 +20,8 @@
* - `header`: Text displayed at the top of the screen
* - `minLength`: Minimum allowed text length
* - `maxLength`: Maximum allowed text length
* - `defaultText`: Text to show by default
* - `defaultTextClear`: Whether to clear the default text on next character typed
*
* @version Added in JS SDK 0.1
* @module
@ -32,6 +34,8 @@ type Props = {
header: string,
minLength: number,
maxLength: number,
defaultText: string,
defaultTextClear: boolean,
}
declare class TextInput extends View<Props> {
input: Contract<string>;

View File

@ -4,6 +4,8 @@
* @module
*/
/** @version Added in JS SDK 0.1 */
export function isEqual(a: number, b: number, tolerance: number): boolean;
/** @version Added in JS SDK 0.1 */
export function abs(n: number): number;
/** @version Added in JS SDK 0.1 */
@ -33,6 +35,8 @@ export function exp(n: number): number;
/** @version Added in JS SDK 0.1 */
export function floor(n: number): number;
/** @version Added in JS SDK 0.1 */
export function log(n: number): number;
/** @version Added in JS SDK 0.1 */
export function max(n: number, m: number): number;
/** @version Added in JS SDK 0.1 */
export function min(n: number, m: number): number;
@ -51,4 +55,6 @@ export function trunc(n: number): number;
/** @version Added in JS SDK 0.1 */
declare const PI: number;
/** @version Added in JS SDK 0.1 */
declare const E: number;
/** @version Added in JS SDK 0.1 */
declare const EPSILON: number;

View File

@ -6,6 +6,9 @@
/**
* @brief Initializes the serial port
*
* Automatically disables Expansion module service to prevent interference.
*
* @param port The port to initialize (`"lpuart"` or `"start"`)
* @param baudRate
* @version Added in JS SDK 0.1
@ -52,6 +55,20 @@ export declare function read(length: number, timeout?: number): string | undefin
*/
export declare function readln(timeout?: number): string;
/**
* @brief Read any available amount of data from the serial port
*
* Can be useful to avoid starving your loop with small reads.
*
* @param timeout The number of time, in milliseconds, after which this function
* will give up and return nothing. If unset, the function will
* wait forever.
* @returns The received data interpreted as ASCII, or `undefined` if 0 bytes
* were read.
* @version Added in JS SDK 0.1
*/
export declare function readAny(timeout?: number): string | undefined;
/**
* @brief Reads data from the serial port
* @param length The number of bytes to read
@ -87,3 +104,9 @@ export declare function readBytes(length: number, timeout?: number): ArrayBuffer
* @version Added in JS SDK 0.1
*/
export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined;
/**
* @brief Deinitializes the serial port, allowing multiple initializations per script run
* @version Added in JS SDK 0.1
*/
export declare function end(): void;

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -223,7 +223,7 @@ math.floor(45.05); // 45
math.floor(45.95); // 45
```
## is_equal
## isEqual
Return true if the difference between numbers `a` and `b` is less than the specified parameter `e`.
### Parameters
@ -236,8 +236,8 @@ True if the difference between numbers `a` and `b` is less than the specified pa
### Example
```js
math.is_equal(1.4, 1.6, 0.2); // false
math.is_equal(3.556, 3.555, 0.01); // true
math.isEqual(1.4, 1.6, 0.2); // false
math.isEqual(3.556, 3.555, 0.01); // true
```
## max

View File

@ -452,6 +452,12 @@ static int getprop_builtin_string(
} else if(strcmp(name, "slice") == 0) {
*res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_slice);
return 1;
} else if(strcmp(name, "toUpperCase") == 0) {
*res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_upper_case);
return 1;
} else if(strcmp(name, "toLowerCase") == 0) {
*res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_lower_case);
return 1;
} else if(isnum) {
/*
* string subscript: return a new one-byte string if the index
@ -469,6 +475,22 @@ static int getprop_builtin_string(
return 0;
}
static int getprop_builtin_number(
struct mjs* mjs,
mjs_val_t val,
const char* name,
size_t name_len,
mjs_val_t* res) {
if(strcmp(name, "toString") == 0) {
*res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_number_to_string);
return 1;
}
(void)val;
(void)name_len;
return 0;
}
static int getprop_builtin_array(
struct mjs* mjs,
mjs_val_t val,
@ -583,6 +605,8 @@ static int getprop_builtin(struct mjs* mjs, mjs_val_t val, mjs_val_t name, mjs_v
} else if(s != NULL && n == 5 && strncmp(s, "apply", n) == 0) {
*res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_apply_);
handled = 1;
} else if(mjs_is_number(val)) {
handled = getprop_builtin_number(mjs, val, s, n, res);
} else if(mjs_is_array(val)) {
handled = getprop_builtin_array(mjs, val, s, n, res);
} else if(mjs_is_foreign(val)) {

View File

@ -6,6 +6,8 @@
#include "mjs_core.h"
#include "mjs_internal.h"
#include "mjs_primitive.h"
#include "mjs_string_public.h"
#include "mjs_util.h"
mjs_val_t mjs_mk_null(void) {
return MJS_NULL;
@ -158,3 +160,31 @@ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs) {
mjs_return(mjs, ret);
}
MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs) {
mjs_val_t ret = MJS_UNDEFINED;
mjs_val_t base_v = MJS_UNDEFINED;
int32_t base = 10;
int32_t num;
/* get number from `this` */
if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_NUMBER, NULL)) {
goto clean;
}
num = mjs_get_int32(mjs, mjs->vals.this_obj);
if(mjs_nargs(mjs) >= 1) {
/* get base from arg 0 */
if(!mjs_check_arg(mjs, 0, "base", MJS_TYPE_NUMBER, &base_v)) {
goto clean;
}
base = mjs_get_int(mjs, base_v);
}
char tmp_str[] = "-2147483648";
itoa(num, tmp_str, base);
ret = mjs_mk_string(mjs, tmp_str, ~0, true);
clean:
mjs_return(mjs, ret);
}

View File

@ -34,6 +34,11 @@ MJS_PRIVATE void* get_ptr(mjs_val_t v);
*/
MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs);
/*
* Implementation for JS Number.toString()
*/
MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs);
#if defined(__cplusplus)
}
#endif /* __cplusplus */

View File

@ -286,6 +286,41 @@ MJS_PRIVATE mjs_val_t s_concat(struct mjs* mjs, mjs_val_t a, mjs_val_t b) {
return res;
}
MJS_PRIVATE void mjs_string_to_case(struct mjs* mjs, bool upper) {
mjs_val_t ret = MJS_UNDEFINED;
size_t size;
const char* s = NULL;
/* get string from `this` */
if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_STRING, NULL)) {
goto clean;
}
s = mjs_get_string(mjs, &mjs->vals.this_obj, &size);
if(size == 0) {
ret = mjs_mk_string(mjs, "", 0, 1);
goto clean;
}
char* tmp = malloc(size);
for(size_t i = 0; i < size; i++) {
tmp[i] = upper ? toupper(s[i]) : tolower(s[i]);
}
ret = mjs_mk_string(mjs, tmp, size, 1);
free(tmp);
clean:
mjs_return(mjs, ret);
}
MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs) {
mjs_string_to_case(mjs, false);
}
MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs) {
mjs_string_to_case(mjs, true);
}
MJS_PRIVATE void mjs_string_slice(struct mjs* mjs) {
int nargs = mjs_nargs(mjs);
mjs_val_t ret = mjs_mk_number(mjs, 0);

View File

@ -33,6 +33,8 @@ MJS_PRIVATE void embed_string(
MJS_PRIVATE void mjs_mkstr(struct mjs* mjs);
MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs);
MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs);
MJS_PRIVATE void mjs_string_slice(struct mjs* mjs);
MJS_PRIVATE void mjs_string_index_of(struct mjs* mjs);
MJS_PRIVATE void mjs_string_char_code_at(struct mjs* mjs);