Skip to content

Commit

Permalink
JS: Backport³ and more additions & fixes (#3961)
Browse files Browse the repository at this point in the history
* 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 1967ec0.
* 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>
  • Loading branch information
7 people authored Oct 31, 2024
1 parent 1907f23 commit c807ffc
Show file tree
Hide file tree
Showing 35 changed files with 1,048 additions and 73 deletions.
29 changes: 23 additions & 6 deletions applications/system/js_app/application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,50 @@ 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"],
)

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"],
)

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"],
)

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"],
)

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,
Expand All @@ -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,
Expand Down Expand Up @@ -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"],
)

Expand Down
19 changes: 17 additions & 2 deletions applications/system/js_app/examples/apps/Scripts/badusb_demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -39,14 +45,23 @@ 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 {
print("USB not connected");
notify.error();
}

// Optional, but allows to unlock usb interface to switch profile
badusb.quit();

eventLoop.stop();
}, eventLoop, gui);

Expand Down
49 changes: 44 additions & 5 deletions applications/system/js_app/examples/apps/Scripts/gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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.",
Expand All @@ -29,7 +37,9 @@ let views = {
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Exit app",
],
}),
Expand All @@ -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);

Expand All @@ -67,10 +90,26 @@ eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views)
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);

// show data after byte input
eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
let data_view = Uint8Array(data);
let text = "0x";
for (let i = 0; i < data_view.length; i++) {
text += data_view[i].toString(16);
}
views.helloDialog.set("text", "You typed:\n" + text);
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);

// go to the demo chooser screen when the back key is pressed
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
eventLoop.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);
}, gui, views, eventLoop);

// run UI
gui.viewDispatcher.switchTo(views.demos);
Expand Down
93 changes: 93 additions & 0 deletions applications/system/js_app/examples/apps/Scripts/interactive.js
Original file line number Diff line number Diff line change
@@ -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 :)")
2 changes: 1 addition & 1 deletion applications/system/js_app/examples/apps/Scripts/load.js
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions applications/system/js_app/examples/apps/Scripts/path.js
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions applications/system/js_app/examples/apps/Scripts/storage.js
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions applications/system/js_app/examples/apps/Scripts/stringutils.js
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion applications/system/js_app/js_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit c807ffc

Please sign in to comment.