Merge branch 'main' of https://github.com/jamisonderek/flipper-zero-tutorials
This commit is contained in:
commit
c07fbbe530
17
js/ai/intro.txt
Normal file
17
js/ai/intro.txt
Normal 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
38
js/ai/readme.md
Normal 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
303
js/ai/train.js
Normal 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
50
js/ai/train2.js
Normal 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
76
js/ai/train3.js
Normal 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!");
|
||||
}
|
Loading…
Reference in New Issue
Block a user