mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2024-12-22 21:01:40 +03:00
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:
parent
1907f23e5f
commit
c807ffc324
@ -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"],
|
||||
)
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 :)")
|
@ -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);
|
||||
|
9
applications/system/js_app/examples/apps/Scripts/path.js
Normal file
9
applications/system/js_app/examples/apps/Scripts/path.js
Normal 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.");
|
||||
}
|
29
applications/system/js_app/examples/apps/Scripts/storage.js
Normal file
29
applications/system/js_app/examples/apps/Scripts/storage.js
Normal 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
|
@ -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);
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
158
applications/system/js_app/modules/js_gui/byte_input.c
Normal file
158
applications/system/js_app/modules/js_gui/byte_input.c
Normal 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);
|
47
applications/system/js_app/modules/js_gui/file_picker.c
Normal file
47
applications/system/js_app/modules/js_gui/file_picker.c
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 { }
|
||||
|
||||
|
41
applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts
vendored
Normal file
41
applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts
vendored
Normal 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;
|
7
applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts
vendored
Normal file
7
applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts
vendored
Normal 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;
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
BIN
assets/icons/Archive/file_10px.png
Normal file
BIN
assets/icons/Archive/file_10px.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 B |
BIN
documentation/images/byte_input.png
Normal file
BIN
documentation/images/byte_input.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -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
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user