From 942748870b076ed4dc1ee0e3ff58e017cf6d4bd8 Mon Sep 17 00:00:00 2001 From: Derek Jamison Date: Wed, 26 Jun 2024 11:32:13 -0500 Subject: [PATCH] ChatGPT training for JavaScript --- js/ai/intro.txt | 17 +++ js/ai/readme.md | 38 ++++++ js/ai/train.js | 303 ++++++++++++++++++++++++++++++++++++++++++++++++ js/ai/train2.js | 50 ++++++++ js/ai/train3.js | 76 ++++++++++++ 5 files changed, 484 insertions(+) create mode 100644 js/ai/intro.txt create mode 100644 js/ai/readme.md create mode 100644 js/ai/train.js create mode 100644 js/ai/train2.js create mode 100644 js/ai/train3.js diff --git a/js/ai/intro.txt b/js/ai/intro.txt new file mode 100644 index 0000000..fdf02d7 --- /dev/null +++ b/js/ai/intro.txt @@ -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. \ No newline at end of file diff --git a/js/ai/readme.md b/js/ai/readme.md new file mode 100644 index 0000000..b889ae0 --- /dev/null +++ b/js/ai/readme.md @@ -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: . +Line 4 is +``` + +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). diff --git a/js/ai/train.js b/js/ai/train.js new file mode 100644 index 0000000..ae58de8 --- /dev/null +++ b/js/ai/train.js @@ -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(); +} + diff --git a/js/ai/train2.js b/js/ai/train2.js new file mode 100644 index 0000000..93bd897 --- /dev/null +++ b/js/ai/train2.js @@ -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(); + diff --git a/js/ai/train3.js b/js/ai/train3.js new file mode 100644 index 0000000..267090f --- /dev/null +++ b/js/ai/train3.js @@ -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!"); +}