Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS: Backport³ and more additions & fixes #3961

Open
wants to merge 34 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
223336f
JS: Fix file select for fbt launch js_app
Willy-JL Oct 17, 2024
68d31dd
JS: badusb: Add numpad keys
Willy-JL Oct 17, 2024
60e96e6
JS: badusb: Layout support
Willy-JL Oct 17, 2024
4cae1e9
JS: badusb: altPrint() and altPrintln()
Willy-JL Oct 17, 2024
efc2494
JS: badusb: quit()
Willy-JL Oct 17, 2024
e8079f8
JS: serial: readAny()
Willy-JL Oct 17, 2024
efca524
JS: serial: end()
Willy-JL Oct 17, 2024
5476395
JS: serial: Auto disable expansion service
Willy-JL Oct 17, 2024
229c77b
JS: storage: Add example script
Willy-JL Oct 17, 2024
d50b062
JS: gui: text_input: Fix NULL ptr when no prop given
Willy-JL Oct 17, 2024
4bbbd29
JS: gui: text_input: Default text props
Willy-JL Oct 17, 2024
f559c2f
JS: gui: byte_input
Willy-JL Oct 17, 2024
78a0544
JS: gui: file_picker
Willy-JL Oct 17, 2024
d05c20b
JS: gui: viewDispatcher.currentView
Willy-JL Oct 17, 2024
1967ec0
JS: gui: view.hasProperty()
Willy-JL Oct 17, 2024
ab49604
JS: gui: Add some missing typedefs comments
Willy-JL Oct 17, 2024
0e75674
JS: globals: Fix toString() with negative numbers
Willy-JL Oct 17, 2024
56e45c8
JS: globals: parseInt()
Willy-JL Oct 17, 2024
1e9fb43
JS: globals: toUpperCase() and toLowerCase()
Willy-JL Oct 17, 2024
76f3a2e
JS: globals: Add some missing typedefs
Willy-JL Oct 17, 2024
0aa3528
JS: Add example for string functions
Willy-JL Oct 17, 2024
00fd305
JS: globals: __dirpath and __filepath
Willy-JL Oct 17, 2024
fe9f428
JS: globals: load() typedef missing scope param
Willy-JL Oct 17, 2024
a694e80
JS: Add interactive REPL script example
Willy-JL Oct 17, 2024
6e599c3
Merge branch 'dev' into js-backport3-and-more
hedger Oct 17, 2024
d2a3c16
JS: Add missing icon for file picker
Willy-JL Oct 17, 2024
d1808e7
JS: Rename to __filename and __dirname
Willy-JL Oct 18, 2024
998fb06
JS: Move toUpperCase() and toLowerCase() to string class
Willy-JL Oct 18, 2024
c95cfcb
JS: parseInt() refactor
Willy-JL Oct 18, 2024
26643d3
JS: Typedef base param for parseInt()
Willy-JL Oct 18, 2024
5c46835
Revert "JS: gui: view.hasProperty()"
Willy-JL Oct 18, 2024
f0a6b6f
JS: Move toString() to Number class
Willy-JL Oct 18, 2024
32f8346
JS: Fix duplicate plugin files
Willy-JL Oct 18, 2024
f8852e4
Merge branch 'dev' into js-backport3-and-more
skotopes Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions applications/system/js_app/application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,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 @@ -82,6 +90,14 @@ 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"],
)

App(
appid="js_notification",
apptype=FlipperAppType.PLUGIN,
Expand Down Expand Up @@ -110,7 +126,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
51 changes: 46 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,20 +5,30 @@ 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 = {
loading: loadingView.make(),
empty: emptyView.make(),
keyboard: textInputView.makeWith({
header: "Enter your name",
defaultText: flipper.getName(),
defaultTextClear: true,
// Props for makeWith() are passed in reverse order, so maxLength must be after defaultText
minLength: 0,
maxLength: 32,
}),
helloDialog: dialogView.makeWith({
center: "Hi Flipper! :)",
helloDialog: dialogView.make(),
bytekb: byteInputView.makeWith({
header: "Look ma, I'm a header text!",
defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]),
// Props for makeWith() are passed in reverse order, so length must be after defaultData
length: 8,
}),
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 +39,9 @@ let views = {
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Exit app",
],
}),
Expand All @@ -49,15 +61,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", "*");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious whether it's at all possible to integrate the file picker with the rest of the GUI system as an ordinary view and make its API asynchronous. Gonna look into it.

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 +92,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
94 changes: 94 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,94 @@
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:",
defaultText: "2+2",
defaultTextClear: true,
// Props for makeWith() are passed in reverse order, so maxLength must be after defaultText
minLength: 0,
maxLength: 256,
}),
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
Loading