This commit is contained in:
Derek Jamison 2024-07-13 09:46:33 -06:00
commit c07fbbe530
5 changed files with 484 additions and 0 deletions

17
js/ai/intro.txt Normal file
View File

@ -0,0 +1,17 @@
Flipper Zero JavaScript is based on the mJS engine, which is fairly restricted. For example:
- You must use the `let` keyword. `const` and `var` are not allowed.
- You cannot do string interpolation with the `$`.
- You must use `let Math = require("math");` if you need access to any of the Math functions.
- You must use `to_string` to convert numerics (like integer and float) into a string.
- When keyboard.text display the on-screen keyboard, it covers the entire screen (so you can't see any previously printed information).
- All files are stored on the SD Card under the `/ext/` subdirectory.
- Delays are always measured in milliseconds and use the built-in `delay` function.
- Uint8Array(buffer) instead of `new Uint8Array(buffer)`. Objects like `String` do not exist. See arraybuf_to_string for code that should be used when converting a buffer into a string.
- There is no split or trim functions build in for strings. "1,2".split(","); and "x ".trim(); will generate runtime errors.
- See the training file for how to use the to_upper_case, to_lower_case, parse_int. You can get the ascii value of a character in a string using charCodeAt and also convert an ascii value back to a char using chr; see training file for details.
- Console.log prints to the serial port debugger, as an [I]nfo. Console.error prints to the serial port debugger, as an [E]rror. The print method writes to the Flipper display.
- Functions must be declared before they are used, but typically after all of the require statements.
Attached are three sample JavaScript files that work on the Flipper Zero.
You should only use functions defined in the training file, no additional built-in commands are supported. You should not redefine any function that is used in the training file.

38
js/ai/readme.md Normal file
View File

@ -0,0 +1,38 @@
# AI for JavaScript
## Overview
This project was tested with [ChatGPT-4o](https://chatgpt.com/?model=gpt-4o). The goal of this project is to use AI to generate JavaScript that runs on the Flipper Zero; without requiring the user to be familiar with JavaScript programming.
## Directions
- Step 1. Open [Chatgpt.com](https://chatgpt.com/?model=gpt-4o) and make sure you have `ChatGPT-4o` selected.
- Step 2. Copy the contents of [intro.txt](./intro.txt) into the Message window of Chat.
- Step 3. Drag the train*.js files into the Message window.
- Step 4. Press `Ctrl`+`Enter` to get a blank line.
- Step 5. Request a script to do some feature for the Flipper Zero! Be sure you request something that is in [Supported Capabilities](https://github.com/jamisonderek/flipper-zero-tutorials/wiki/JavaScript#capabilities) of JavaScript; or at least not in the [Not Supported](https://github.com/jamisonderek/flipper-zero-tutorials/wiki/JavaScript#not-supported) for the best results.
Example: "Create a JavaScript program for the Flipper Zero that prints 'Hello World' on the screen."
- Step 6. Click the `Copy code` button from the response.
- Step 7. Paste the resulting script into a text file (I named my file `ai.js`).
- Step 8. Copy the text file to your Flipper Zero on the SD Card at `sd card/apps/Scripts`.
- Step 9. Run the script on the Flipper Zero (`Apps`, `Scripts`, *yourScript.js*).
## Troubleshooting
If you get an error displayed on Flipper...
Tell ChatGPT
```
I got error: <message from flipper:4>.
Line 4 is <contents of line from js file>
```
Replace the stuff in angle brackets with the actual error message. The end of the error will have a `:` and line number (4 in this example). Replace the second line with the actual line number reported and the contents of the line (Ctrl+G goes to line in Notepad).
If program runs, but does not give expected result...
Tell ChatGPT the result you got and the expected result.
## Support
If you have any questions, please ask in my [Flipper Zero Discord](https://discord.com/invite/NsjCvqwPAd) server. There are also giveaways and other fun things happening there.
Support my work:
- Option 1. [Like, Subscribe and click the Bell (to get notified)](https://youtu.be/DAUQGeG4pc4)
- Option 2. [https://ko-fi.com/codeallnight](https://ko-fi.com/codeallnight) (donate $3 via PayPal or Venmo)
- Option 3. Click the "Thanks" button on [YouTube](https://youtu.be/DAUQGeG4pc4).
- Option 4. Purchase a [FlipBoard](https://github.com/MakeItHackin/FlipBoard) (I get a portion of the sale).

303
js/ai/train.js Normal file
View File

@ -0,0 +1,303 @@
// Print a message on the display
print("print", 1);
// Print a message to the debugger serial port
console.log("log", 2);
let gpio = require("gpio");
// initialize pin A4 as analog (you can also use PA7, PA6, PC3, PC1, PC0)
gpio.init("PA4", "analog", "no"); // pin, mode, pull
// Set max input voltage to 2.048V (2048mV)
// You can also use 2500 for a 2.5V (2500mV) max input.
gpio.startAnalog(2048);
// Returns the number of millivolts (or the reference voltage if it exceeds the reference voltage)
let pa4_value = gpio.readAnalog("PA4");
print("A4 is " + to_string(pa4_value) + "mV");
gpio.stopAnalog();
let arr_1 = Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Prints "len = 10"
print("len =", arr_1.buffer.byteLength);
let arr_2 = Uint8Array(arr_1.buffer.slice(2, 6));
// Prints "slice len = 4"
print("slice len =", arr_2.buffer.byteLength);
// Prints 2,3,4,5 on separate lines.
for (let i = 0; i < arr_2.buffer.byteLength; i++) {
print(arr_2[i]);
}
let serial = require("serial");
serial.setup("lpuart", 115200);
// serial.write("\n");
serial.write([0x0a]);
let console_resp = serial.expect("# ", 1000);
if (console_resp === undefined) {
print("No CLI response");
} else {
serial.write("uci\n");
let uci_state = serial.expect([": not found", "Usage: "]);
if (uci_state === 1) {
serial.expect("# ");
serial.write("uci show wireless\n");
serial.expect(".key=");
print("key:", serial.readln());
} else {
print("uci cmd not found");
}
}
// 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
let badusb = require("badusb");
let notify = require("notification");
let flipper = require("flipper");
let dialog = require("dialog");
badusb.setup({
vid: 0xAAAA,
pid: 0xBBBB,
mfr_name: "Flipper",
prod_name: "Zero",
layout_path: "/ext/badusb/assets/layouts/en-US.kl"
});
dialog.message("BadUSB demo", "Press OK to start");
if (badusb.isConnected()) {
notify.blink("green", "short");
print("USB is connected");
// Act like user typed "Hello, World!" and pressed enter.
badusb.println("Hello, world!", 10); // 10 ms between characters improves typing.
// Act like user pressed CTRL+A keys.
badusb.press("CTRL", "a");
delay(1000);
// Act like user typed the name of their Flipper Zero!
badusb.println(flipper.getName());
// happy sound + vibro + green status light.
notify.success();
} else {
print("USB not connected");
// beep,beep + vibro + red status light.
notify.error();
}
// Optional, but allows to interchange with usbdisk
badusb.quit();
//dialog already imported above.
//let dialog = require("dialog");
let result1 = dialog.message("Dialog demo", "Press OK to start");
// prints true if user presses ok.
print(result1);
let dialog_params = ({
header: "Test_header",
text: "Test_text",
button_left: "Left",
button_right: "Files",
button_center: "OK"
});
let result2 = dialog.custom(dialog_params);
if (result2 === "") {
print("Back is pressed");
} else if (result2 === "Files") {
let result3 = dialog.pickFile("/ext", "*");
print("Selected", result3);
} else {
print(result2, "is pressed");
}
// gpio is already defined
//let gpio = require("gpio");
// initialize pins
// Pins of the Flipper Zero are:
// 1 : 5 volts (Normally disconnected, must be enabled in GPIO settings or plug in USB-C cable)
// 2 : PA7 (SPI MOSI & ADC 12)
// 3 : PA6 (SPI MISO & ADC 11)
// 4 : PA4 (SPI CS & ADC 9)
// 5 : PB3 (SPI SCK)
// 6 : PB2 (SPI other)
// 7 : PC3 (ADC 4)
// 8 : GND
// 9 : 3.3 volts
// 10 : PA14 (SWC : software debugger clock)
// 11 : GND
// 12 : PA13 (SIO : software debugger i/o)
// 13 : PB6 (TX : USART transmit)
// 14 : PB7 (RX : USART receive)
// 15 : PC1 (I2C SDA & LPUART TX & ADC 2)
// 16 : PC0 (I2C SCL & LPUART RX & ADC 1)
// 17 : PB14 (1Wire / iBUTTON : has 1K pull-up resistor)
// 18 : GND
// 20 mA max current per GPIO pin (2-7, 10, 12-17)
// 1 Amp max current for 3.3 volt pin
// 1 Amp max current for 5.0 volt pin
gpio.init("PC3", "outputPushPull", "up"); // pin, mode, pull
// pull parameter is "up"/"down"/"no".
print("PC3 is initialized as outputPushPull with pull-up");
gpio.init("PC1", "input", "down"); // pin, mode, pull
print("PC1 is initialized as input with pull-down");
// let led on PC3 blink
gpio.write("PC3", true); // high
delay(1000);
gpio.write("PC3", false); // low
delay(1000);
// read value from PC1 and write it to PC3
for (let i = 0; i < 10; i++) {
let value = gpio.read("PC1");
gpio.write("PC3", value);
value ? print("PC1 is high") : print("PC1 is low");
delay(100);
}
let keyboard = require("keyboard");
keyboard.setHeader("Example Text Input");
// Default text is optional, pass "" if unused.
// Third param is true if text should be selected.
// NOTE: When the keyboard is displayed it takes the full screen, so any print messages are not visible. Only the header and default text are visible.
let text = keyboard.text(100, "Default text", true);
// Returns undefined when pressing back
print("Got text:", text);
keyboard.setHeader("Example Byte Input");
// Default data is optional
let result4 = keyboard.byte(6, Uint8Array([1, 2, 3, 4, 5, 6]));
// Returns undefined when pressing back
if (result4 !== undefined) {
let data = Uint8Array(result4);
result4 = "0x";
for (let i = 0; i < data.byteLength; i++) {
if (data[i] < 0x10) result4 += "0";
result4 += to_hex_string(data[i]);
}
}
print("Got data:", result4);
let math = require("math");
print("math.abs(-5):", math.abs(-5));
print("math.acos(0.5):", math.acos(0.5));
print("math.acosh(2):", math.acosh(2));
print("math.asin(0.5):", math.asin(0.5));
print("math.asinh(2):", math.asinh(2));
print("math.atan(1):", math.atan(1));
print("math.atan2(1, 1):", math.atan2(1, 1));
print("math.atanh(0.5):", math.atanh(0.5));
print("math.cbrt(27):", math.cbrt(27));
print("math.ceil(5.3):", math.ceil(5.3));
print("math.clz32(1):", math.clz32(1));
print("math.cos(math.PI):", math.cos(math.PI));
print("math.exp(1):", math.exp(1));
print("math.floor(5.7):", math.floor(5.7));
print("math.max(3, 5):", math.max(3, 5));
print("math.min(3, 5):", math.min(3, 5));
print("math.pow(2, 3):", math.pow(2, 3));
print("math.random():", math.random());
print("math.sign(-5):", math.sign(-5));
print("math.sin(math.PI/2):", math.sin(math.PI / 2));
print("math.sqrt(25):", math.sqrt(25));
print("math.trunc(5.7):", math.trunc(5.7));
let storage = require("storage");
print("script has __dirpath of" + __dirpath);
print("script has __filepath of" + __filepath);
if (storage.exists(__dirpath + "/math.js")) {
print("math.js exist here.");
} else {
print("math.js does not exist here.");
}
//storage already defined above.
//let storage = require("storage");
let path = "/ext/storage.test";
function arraybuf_to_string(arraybuf) {
let string = "";
let data_view = Uint8Array(arraybuf);
for (let i = 0; i < data_view.length; i++) {
string += chr(data_view[i]);
}
return string;
}
print("File exists:", storage.exists(path));
print("Writing...");
// write(path, data, offset)
// If offset is specified, the file is not cleared, content is kept and data is written at specified offset
// Takes both strings and array buffers
storage.write(path, "Hello ");
print("File exists:", storage.exists(path));
// Append will create the file even if it doesnt exist!
// Takes both strings and array buffers
storage.append(path, "World!");
print("Reading...");
// read(path, size, offset)
// If no size specified, total filesize is used
// If offset is specified, size is capped at (filesize - offset)
let data = storage.read(path);
// read returns an array buffer, to allow proper usage of raw binary data
print(arraybuf_to_string(data));
print("Removing...")
storage.remove(path);
print("Done")
// There's also:
// storage.copy(old_path, new_path);
// storage.move(old_path, new_path);
// storage.mkdir(path);
// storage.virtualInit(path);
// storage.virtualMount();
// storage.virtualQuit();
let sampleText = "Hello, World!";
let lengthOfText = "Length of text: " + to_string(sampleText.length);
print(lengthOfText);
let start = 7;
let end = 12;
let substringResult = sampleText.slice(start, end);
print(substringResult);
let searchStr = "World";
let result5 = to_string(sampleText.indexOf(searchStr));
print(result5);
let upperCaseText = "Text in upper case: " + to_upper_case(sampleText);
print(upperCaseText);
let lowerCaseText = "Text in lower case: " + to_lower_case(sampleText);
print(lowerCaseText);
let code = "123ABC".charCodeAt(3);
let ch = chr(code);
print ("code = " + to_string(code)); // Prints: "code = 65"
print ("ch = " + to_string(ch)); // Prints: "ch = A"
let submenu = require("submenu");
submenu.addItem("Item 1", 0);
submenu.addItem("Item 2", 1);
submenu.addItem("Item 3", 2);
submenu.setHeader("Select an option:");
let result7 = submenu.show();
// Returns undefined when pressing back
// Prints 0-2 depending on user selection.
print("Result:", result7);
let textbox = require("textbox");
// You should set config before adding text
// Focus (start / end), Font (text / hex)
textbox.setConfig("end", "text");
// Can make sure it's cleared before showing, in case of reusing in same script
// (Closing textbox already clears the text, but maybe you added more in a loop for example)
textbox.clearText();
// Add default text
textbox.addText("Example dynamic updating textbox\n");
// Non-blocking, can keep updating text after, can close in JS or in GUI
textbox.show();
let i = 0;
while (textbox.isOpen() && i < 5) {
print("console", i++);
// Add text to textbox buffer
textbox.addText("textbox " + to_string(i) + "\n");
delay(500);
}
// If not closed by user (instead i < 5 is false above), close forcefully
if (textbox.isOpen()) {
textbox.close();
}

50
js/ai/train2.js Normal file
View File

@ -0,0 +1,50 @@
let storage = require("storage");
// The Flipper Zero can act like a USB drive!
let usbdisk = require("usbdisk");
if (!storage.exists("/ext/apps_data/mass_storage/8MB.img")) {
print("Creating image...");
usbdisk.createImage("/ext/apps_data/mass_storage/8MB.img", 8 * 1024 * 1024);
}
print("Starting UsbDisk...");
usbdisk.start("/ext/apps_data/mass_storage/8MB.img");
print("Started, waiting until ejected...");
while (!usbdisk.wasEjected()) {
delay(1000);
}
print("Ejected, stopping UsbDisk...");
usbdisk.stop();
print("Done");
// The Flipper Zero supports a Sub-GHz radio
// 315MHz, 390MHz, 433MHz, 868Hz, and 900-915MHz!
let subghz = require("subghz");
subghz.setup();
function printRXline() {
if (subghz.getState() !== "RX") {
subghz.setRx(); // to RX
}
let rssi = subghz.getRssi();
let freq = subghz.getFrequency();
let ext = subghz.isExternal();
print("rssi: ", rssi, "dBm", "@", freq, "MHz", "ext: ", ext);
}
function changeFrequency(freq) {
if (subghz.getState() !== "IDLE") {
subghz.setIdle(); // need to be idle to change frequency
}
subghz.setFrequency(freq);
}
subghz.setIdle();
print(subghz.getState()); // "IDLE"
subghz.setRx();
print(subghz.getState()); // "RX"
changeFrequency(433920000);
printRXline();
delay(1000);
let result6 = subghz.transmitFile("/ext/subghz/0.sub");
print(result6 ? "Send success" : "Send failed");
delay(1000);
changeFrequency(315000000);
printRXline();

76
js/ai/train3.js Normal file
View File

@ -0,0 +1,76 @@
let widget = require("widget");
let demo_seconds = 30;
print("Loading file", __filepath);
print("From directory", __dirpath);
// addText supports "Primary" and "Secondary" font sizes.
widget.addText(10, 10, "Primary", "Example JS widget");
widget.addText(10, 20, "Secondary", "Example widget from JS!");
// load a Xbm file from the same directory as this script.
widget.addText(0, 30, "Secondary", __filepath);
// add a line (x1, y1, x2, y2)
widget.addLine(10, 35, 120, 35);
// add a circle/disc (x, y, radius)
widget.addCircle(12, 52, 10);
let disc = widget.addDisc(12, 52, 5);
// add a frame/box (x, y, width, height)
widget.addFrame(30, 45, 10, 10);
widget.addBox(32, 47, 6, 6);
// add a rounded frame/box (x, y, width, height, radius)
widget.addRframe(50, 45, 15, 15, 3);
widget.addRbox(53, 48, 6, 6, 2);
// add a dot (x, y)
widget.addDot(100, 45);
widget.addDot(102, 44);
widget.addDot(104, 43);
// add an icon (x, y, icon)
//widget.addIcon(100, 50, "ButtonUp_7x4");
//widget.addIcon(100, 55, "ButtonDown_7x4");
// add a glyph (x, y, glyph)
widget.addGlyph(115, 50, "#".charCodeAt(0));
// Show the widget (drawing the layers in the orderer they were added)
widget.show();
let i = 1;
while (widget.isOpen() && i <= demo_seconds) {
// Print statements will only show up once the widget is closed.
print("count is at", i++);
// You can call remove on any added item, it does not impact the other ids.
if (disc) { widget.remove(disc); disc = undefined; }
// All of the addXXX functions return an id that can be used to remove the item.
else { disc = widget.addDisc(12, 52, 5); }
delay(1000);
}
// If user did not press the back button, close the widget.
if (widget.isOpen()) {
widget.close();
}
let vgm = require("vgm");
print ("pitch: " + to_string(vgm.getPitch()));
print ("roll: " + to_string(vgm.getRoll()));
print ("yaw: " + to_string(vgm.getYaw()));
print ("Waiting up to 5 seconds for 15 degree rotation...");
let amount_yaw = vgm.deltaYaw(15, 5000);
if (amount_yaw === 0) {
print ("Timed out!");
} else if (amount_yaw > 0) {
print ("Rotated clockwise " + to_string(amount_yaw) + " degrees!");
} else {
print ("Rotated counter-clockwise " + to_string(0 - amount_yaw) + " degrees!");
}