Skip to content

Commit c807ffc

Browse files
WillyJLoldipxMasterXSpooks4576jamisonderek
authored
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 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>
1 parent 1907f23 commit c807ffc

35 files changed

+1048
-73
lines changed

applications/system/js_app/application.fam

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,42 +41,50 @@ App(
4141
appid="js_gui",
4242
apptype=FlipperAppType.PLUGIN,
4343
entry_point="js_gui_ep",
44-
requires=["js_app", "js_event_loop"],
44+
requires=["js_app"],
4545
sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"],
4646
)
4747

4848
App(
4949
appid="js_gui__loading",
5050
apptype=FlipperAppType.PLUGIN,
5151
entry_point="js_view_loading_ep",
52-
requires=["js_app", "js_gui", "js_event_loop"],
52+
requires=["js_app"],
5353
sources=["modules/js_gui/loading.c"],
5454
)
5555

5656
App(
5757
appid="js_gui__empty_screen",
5858
apptype=FlipperAppType.PLUGIN,
5959
entry_point="js_view_empty_screen_ep",
60-
requires=["js_app", "js_gui", "js_event_loop"],
60+
requires=["js_app"],
6161
sources=["modules/js_gui/empty_screen.c"],
6262
)
6363

6464
App(
6565
appid="js_gui__submenu",
6666
apptype=FlipperAppType.PLUGIN,
6767
entry_point="js_view_submenu_ep",
68-
requires=["js_app", "js_gui"],
68+
requires=["js_app"],
6969
sources=["modules/js_gui/submenu.c"],
7070
)
7171

7272
App(
7373
appid="js_gui__text_input",
7474
apptype=FlipperAppType.PLUGIN,
7575
entry_point="js_view_text_input_ep",
76-
requires=["js_app", "js_gui", "js_event_loop"],
76+
requires=["js_app"],
7777
sources=["modules/js_gui/text_input.c"],
7878
)
7979

80+
App(
81+
appid="js_gui__byte_input",
82+
apptype=FlipperAppType.PLUGIN,
83+
entry_point="js_view_byte_input_ep",
84+
requires=["js_app"],
85+
sources=["modules/js_gui/byte_input.c"],
86+
)
87+
8088
App(
8189
appid="js_gui__text_box",
8290
apptype=FlipperAppType.PLUGIN,
@@ -93,6 +101,15 @@ App(
93101
sources=["modules/js_gui/dialog.c"],
94102
)
95103

104+
App(
105+
appid="js_gui__file_picker",
106+
apptype=FlipperAppType.PLUGIN,
107+
entry_point="js_gui_file_picker_ep",
108+
requires=["js_app"],
109+
sources=["modules/js_gui/file_picker.c"],
110+
fap_libs=["assets"],
111+
)
112+
96113
App(
97114
appid="js_notification",
98115
apptype=FlipperAppType.PLUGIN,
@@ -121,7 +138,7 @@ App(
121138
appid="js_gpio",
122139
apptype=FlipperAppType.PLUGIN,
123140
entry_point="js_gpio_ep",
124-
requires=["js_app", "js_event_loop"],
141+
requires=["js_app"],
125142
sources=["modules/js_gpio.c"],
126143
)
127144

applications/system/js_app/examples/apps/Scripts/badusb_demo.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ let views = {
1313
}),
1414
};
1515

16-
badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" });
16+
badusb.setup({
17+
vid: 0xAAAA,
18+
pid: 0xBBBB,
19+
mfrName: "Flipper",
20+
prodName: "Zero",
21+
layoutPath: "/ext/badusb/assets/layouts/en-US.kl"
22+
});
1723

1824
eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) {
1925
if (button !== "center")
@@ -39,14 +45,23 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui)
3945

4046
badusb.println("Flipper Model: " + flipper.getModel());
4147
badusb.println("Flipper Name: " + flipper.getName());
42-
badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%");
48+
badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%");
49+
50+
// Alt+Numpad method works only on Windows!!!
51+
badusb.altPrintln("This was printed with Alt+Numpad method!");
52+
53+
// There's also badusb.print() and badusb.altPrint()
54+
// which don't add the return at the end
4355

4456
notify.success();
4557
} else {
4658
print("USB not connected");
4759
notify.error();
4860
}
4961

62+
// Optional, but allows to unlock usb interface to switch profile
63+
badusb.quit();
64+
5065
eventLoop.stop();
5166
}, eventLoop, gui);
5267

applications/system/js_app/examples/apps/Scripts/gui.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ let loadingView = require("gui/loading");
55
let submenuView = require("gui/submenu");
66
let emptyView = require("gui/empty_screen");
77
let textInputView = require("gui/text_input");
8+
let byteInputView = require("gui/byte_input");
89
let textBoxView = require("gui/text_box");
910
let dialogView = require("gui/dialog");
11+
let filePicker = require("gui/file_picker");
12+
let flipper = require("flipper");
1013

1114
// declare view instances
1215
let views = {
@@ -16,9 +19,14 @@ let views = {
1619
header: "Enter your name",
1720
minLength: 0,
1821
maxLength: 32,
22+
defaultText: flipper.getName(),
23+
defaultTextClear: true,
1924
}),
20-
helloDialog: dialogView.makeWith({
21-
center: "Hi Flipper! :)",
25+
helloDialog: dialogView.make(),
26+
bytekb: byteInputView.makeWith({
27+
header: "Look ma, I'm a header text!",
28+
length: 8,
29+
defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]),
2230
}),
2331
longText: textBoxView.makeWith({
2432
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 = {
2937
"Hourglass screen",
3038
"Empty screen",
3139
"Text input & Dialog",
40+
"Byte input",
3241
"Text box",
42+
"File picker",
3343
"Exit app",
3444
],
3545
}),
@@ -49,15 +59,28 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
4959
} else if (index === 2) {
5060
gui.viewDispatcher.switchTo(views.keyboard);
5161
} else if (index === 3) {
52-
gui.viewDispatcher.switchTo(views.longText);
62+
gui.viewDispatcher.switchTo(views.bytekb);
5363
} else if (index === 4) {
64+
gui.viewDispatcher.switchTo(views.longText);
65+
} else if (index === 5) {
66+
let path = filePicker.pickFile("/ext", "*");
67+
if (path) {
68+
views.helloDialog.set("text", "You selected:\n" + path);
69+
} else {
70+
views.helloDialog.set("text", "You didn't select a file");
71+
}
72+
views.helloDialog.set("center", "Nice!");
73+
gui.viewDispatcher.switchTo(views.helloDialog);
74+
} else if (index === 6) {
5475
eventLoop.stop();
5576
}
5677
}, gui, eventLoop, views);
5778

5879
// say hi after keyboard input
5980
eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) {
81+
views.keyboard.set("defaultText", name); // Remember for next usage
6082
views.helloDialog.set("text", "Hi " + name + "! :)");
83+
views.helloDialog.set("center", "Hi Flipper! :)");
6184
gui.viewDispatcher.switchTo(views.helloDialog);
6285
}, gui, views);
6386

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

93+
// show data after byte input
94+
eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) {
95+
let data_view = Uint8Array(data);
96+
let text = "0x";
97+
for (let i = 0; i < data_view.length; i++) {
98+
text += data_view[i].toString(16);
99+
}
100+
views.helloDialog.set("text", "You typed:\n" + text);
101+
views.helloDialog.set("center", "Cool!");
102+
gui.viewDispatcher.switchTo(views.helloDialog);
103+
}, gui, views);
104+
70105
// go to the demo chooser screen when the back key is pressed
71-
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
106+
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views, eventLoop) {
107+
if (gui.viewDispatcher.currentView === views.demos) {
108+
eventLoop.stop();
109+
return;
110+
}
72111
gui.viewDispatcher.switchTo(views.demos);
73-
}, gui, views);
112+
}, gui, views, eventLoop);
74113

75114
// run UI
76115
gui.viewDispatcher.switchTo(views.demos);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
let eventLoop = require("event_loop");
2+
let gui = require("gui");
3+
let dialog = require("gui/dialog");
4+
let textInput = require("gui/text_input");
5+
let loading = require("gui/loading");
6+
let storage = require("storage");
7+
8+
// No eval() or exec() so need to run code from file, and filename must be unique
9+
storage.makeDirectory("/ext/.tmp");
10+
storage.makeDirectory("/ext/.tmp/js");
11+
storage.rmrf("/ext/.tmp/js/repl")
12+
storage.makeDirectory("/ext/.tmp/js/repl")
13+
let ctx = {
14+
tmpTemplate: "/ext/.tmp/js/repl/",
15+
tmpNumber: 0,
16+
persistentScope: {},
17+
};
18+
19+
let views = {
20+
dialog: dialog.makeWith({
21+
header: "Interactive Console",
22+
text: "Press OK to Start",
23+
center: "Run Some JS"
24+
}),
25+
textInput: textInput.makeWith({
26+
header: "Type JavaScript Code:",
27+
minLength: 0,
28+
maxLength: 256,
29+
defaultText: "2+2",
30+
defaultTextClear: true,
31+
}),
32+
loading: loading.make(),
33+
};
34+
35+
eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) {
36+
if (button === "center") {
37+
gui.viewDispatcher.switchTo(views.textInput);
38+
}
39+
}, gui, views);
40+
41+
eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) {
42+
gui.viewDispatcher.switchTo(views.loading);
43+
44+
let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString();
45+
let file = storage.openFile(path, "w", "create_always");
46+
file.write(text);
47+
file.close();
48+
49+
// Hide GUI before running, we want to see console and avoid deadlock if code fails
50+
gui.viewDispatcher.sendTo("back");
51+
let result = load(path, ctx.persistentScope); // Load runs JS and returns last value on stack
52+
storage.remove(path);
53+
54+
// Must convert to string explicitly
55+
if (result === null) { // mJS: typeof null === "null", ECMAScript: typeof null === "object", IDE complains when checking "null" type
56+
result = "null";
57+
} else if (typeof result === "string") {
58+
result = "'" + result + "'";
59+
} else if (typeof result === "number") {
60+
result = result.toString();
61+
} else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check
62+
result = "bigint";
63+
} else if (typeof result === "boolean") {
64+
result = result ? "true" : "false";
65+
} else if (typeof result === "symbol") { // mJS doesn't support Symbol() but might aswell check
66+
result = "symbol";
67+
} else if (typeof result === "undefined") {
68+
result = "undefined";
69+
} else if (typeof result === "object") {
70+
result = "object"; // JSON.stringify() is not implemented
71+
} else if (typeof result === "function") {
72+
result = "function";
73+
} else {
74+
result = "unknown type: " + typeof result;
75+
}
76+
77+
gui.viewDispatcher.sendTo("front");
78+
views.dialog.set("header", "JS Returned:");
79+
views.dialog.set("text", result);
80+
gui.viewDispatcher.switchTo(views.dialog);
81+
views.textInput.set("defaultText", text);
82+
}, gui, views, ctx);
83+
84+
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) {
85+
eventLoop.stop();
86+
}, eventLoop);
87+
88+
gui.viewDispatcher.switchTo(views.dialog);
89+
90+
// Message behind GUI if something breaks
91+
print("If you're stuck here, something went wrong, re-run the script")
92+
eventLoop.run();
93+
print("\n\nFinished correctly :)")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
let math = load("/ext/apps/Scripts/load_api.js");
1+
let math = load(__dirname + "/load_api.js");
22
let result = math.add(5, 10);
33
print(result);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
let storage = require("storage");
2+
3+
print("script has __dirname of" + __dirname);
4+
print("script has __filename of" + __filename);
5+
if (storage.fileExists(__dirname + "/math.js")) {
6+
print("math.js exist here.");
7+
} else {
8+
print("math.js does not exist here.");
9+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
let storage = require("storage");
2+
let path = "/ext/storage.test";
3+
4+
print("File exists:", storage.fileExists(path));
5+
6+
print("Writing...");
7+
let file = storage.openFile(path, "w", "create_always");
8+
file.write("Hello ");
9+
file.close();
10+
11+
print("File exists:", storage.fileExists(path));
12+
13+
file = storage.openFile(path, "w", "open_append");
14+
file.write("World!");
15+
file.close();
16+
17+
print("Reading...");
18+
file = storage.openFile(path, "r", "open_existing");
19+
let text = file.read("ascii", 128);
20+
file.close();
21+
print(text);
22+
23+
print("Removing...")
24+
storage.remove(path);
25+
26+
print("Done")
27+
28+
// You don't need to close the file after each operation, this is just to show some different ways to use the API
29+
// There's also many more functions and options, check type definitions in firmware repo
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
let sampleText = "Hello, World!";
2+
3+
let lengthOfText = "Length of text: " + sampleText.length.toString();
4+
print(lengthOfText);
5+
6+
let start = 7;
7+
let end = 12;
8+
let substringResult = sampleText.slice(start, end);
9+
print(substringResult);
10+
11+
let searchStr = "World";
12+
let result2 = sampleText.indexOf(searchStr).toString();
13+
print(result2);
14+
15+
let upperCaseText = "Text in upper case: " + sampleText.toUpperCase();
16+
print(upperCaseText);
17+
18+
let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase();
19+
print(lowerCaseText);

applications/system/js_app/examples/apps/Scripts/uart_echo.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ while (1) {
66
if (rx_data !== undefined) {
77
serial.write(rx_data);
88
let data_view = Uint8Array(rx_data);
9-
print("0x" + toString(data_view[0], 16));
9+
print("0x" + data_view[0].toString(16));
1010
}
1111
}
12+
13+
// There's also serial.end(), so you can serial.setup() again in same script
14+
// You can also use serial.readAny(timeout), will avoid starving your loop with single byte reads

applications/system/js_app/js_app.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ static void js_app_free(JsApp* app) {
9797
int32_t js_app(void* arg) {
9898
JsApp* app = js_app_alloc();
9999

100-
FuriString* script_path = furi_string_alloc_set(APP_ASSETS_PATH());
100+
FuriString* script_path = furi_string_alloc_set(EXT_PATH("apps/Scripts"));
101101
do {
102102
if(arg != NULL && strlen(arg) > 0) {
103103
furi_string_set(script_path, (const char*)arg);

0 commit comments

Comments
 (0)