diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 0b9b1f51f3..961834dcc5 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -86,3 +86,11 @@ App( requires=["js_app"], sources=["modules/js_subghz/*.c"], ) + +App( + appid="js_gpio", + apptype=FlipperAppType.PLUGIN, + entry_point="js_gpio_ep", + requires=["js_app"], + sources=["modules/js_gpio.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/dialog.js b/applications/system/js_app/examples/apps/Scripts/dialog.js index 9fc44f8b9e..4c5b0af202 100644 --- a/applications/system/js_app/examples/apps/Scripts/dialog.js +++ b/applications/system/js_app/examples/apps/Scripts/dialog.js @@ -7,13 +7,16 @@ let dialog_params = ({ header: "Test_header", text: "Test_text", button_left: "Left", - button_right: "Right", + 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"); } diff --git a/applications/system/js_app/examples/apps/Scripts/gpio.js b/applications/system/js_app/examples/apps/Scripts/gpio.js new file mode 100644 index 0000000000..02b55a3adc --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/gpio.js @@ -0,0 +1,62 @@ +let gpio = require("gpio"); + +// initialize pins +gpio.init("PC3", "outputPushPull", "up"); // pin, mode, pull +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); +gpio.write("PC3", true); // high +delay(1000); +gpio.write("PC3", false); // low + +// read value from PC1 and write it to PC3 +while (true) { + let value = gpio.read("PC1"); + gpio.write("PC3", value); + + value ? print("PC1 is high") : print("PC1 is low"); + + delay(100); +} + + +// possible pins https://docs.flipper.net/gpio-and-modules#miFsS +// "PA7" aka 2 +// "PA6" aka 3 +// "PA4" aka 4 +// "PB3" aka 5 +// "PB2" aka 6 +// "PC3" aka 7 +// "PA14" aka 10 +// "PA13" aka 12 +// "PB6" aka 13 +// "PB7" aka 14 +// "PC1" aka 15 +// "PC0" aka 16 +// "PB14" aka 17 + +// possible modes +// "input" +// "outputPushPull" +// "outputOpenDrain" +// "altFunctionPushPull" +// "altFunctionOpenDrain" +// "analog" +// "interruptRise" +// "interruptFall" +// "interruptRiseFall" +// "eventRise" +// "eventFall" +// "eventRiseFall" + +// possible pull +// "no" +// "up" +// "down" \ No newline at end of file diff --git a/applications/system/js_app/modules/js_dialog.c b/applications/system/js_app/modules/js_dialog.c index 34de6d6416..a457d700c9 100644 --- a/applications/system/js_app/modules/js_dialog.c +++ b/applications/system/js_app/modules/js_dialog.c @@ -1,6 +1,7 @@ #include #include "../js_modules.h" #include +#include static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) { size_t num_args = mjs_nargs(mjs); @@ -128,10 +129,62 @@ static void js_dialog_custom(struct mjs* mjs) { } } +static void js_dialog_pick_file(struct mjs* mjs) { + if(mjs_nargs(mjs) != 2) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Wrong arguments"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + mjs_val_t base_path_obj = mjs_arg(mjs, 0); + if(!mjs_is_string(base_path_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base path must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + size_t base_path_len = 0; + const char* base_path = mjs_get_string(mjs, &base_path_obj, &base_path_len); + if((base_path_len == 0) || (base_path == NULL)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad base path argument"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + mjs_val_t extension_obj = mjs_arg(mjs, 1); + if(!mjs_is_string(extension_obj)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Extension must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + size_t extension_len = 0; + const char* extension = mjs_get_string(mjs, &extension_obj, &extension_len); + if((extension_len == 0) || (extension == NULL)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad extension argument"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + const DialogsFileBrowserOptions browser_options = { + .extension = extension, + .icon = &I_Apps_10px, + .base_path = base_path, + }; + FuriString* path = furi_string_alloc_set(base_path); + if(dialog_file_browser_show(dialogs, path, path, &browser_options)) { + mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true)); + } else { + mjs_return(mjs, MJS_UNDEFINED); + } + furi_string_free(path); + furi_record_close(RECORD_DIALOGS); +} + static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) { mjs_val_t dialog_obj = mjs_mk_object(mjs); mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message)); mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom)); + mjs_set(mjs, dialog_obj, "pickFile", ~0, MJS_MK_FN(js_dialog_pick_file)); *object = dialog_obj; return (void*)1; diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c new file mode 100644 index 0000000000..8f61584185 --- /dev/null +++ b/applications/system/js_app/modules/js_gpio.c @@ -0,0 +1,272 @@ +#include "../js_modules.h" +#include +#include +#include + +typedef struct { + const GpioPin* pin; + const char* name; +} GpioPinCtx; + +static const GpioPinCtx js_gpio_pins[] = { + {.pin = &gpio_ext_pa7, .name = "PA7"}, // 2 + {.pin = &gpio_ext_pa6, .name = "PA6"}, // 3 + {.pin = &gpio_ext_pa4, .name = "PA4"}, // 4 + {.pin = &gpio_ext_pb3, .name = "PB3"}, // 5 + {.pin = &gpio_ext_pb2, .name = "PB2"}, // 6 + {.pin = &gpio_ext_pc3, .name = "PC3"}, // 7 + {.pin = &gpio_swclk, .name = "PA14"}, // 10 + {.pin = &gpio_swdio, .name = "PA13"}, // 12 + {.pin = &gpio_usart_tx, .name = "PB6"}, // 13 + {.pin = &gpio_usart_rx, .name = "PB7"}, // 14 + {.pin = &gpio_ext_pc1, .name = "PC1"}, // 15 + {.pin = &gpio_ext_pc0, .name = "PC0"}, // 16 + {.pin = &gpio_ibutton, .name = "PB14"}, // 17 +}; + +bool js_gpio_get_gpio_pull(const char* pull, GpioPull* value) { + if(strcmp(pull, "no") == 0) { + *value = GpioPullNo; + return true; + } else if(strcmp(pull, "up") == 0) { + *value = GpioPullUp; + return true; + } else if(strcmp(pull, "down") == 0) { + *value = GpioPullDown; + return true; + } else { + *value = GpioPullNo; + return true; + } + return false; +} + +bool js_gpio_get_gpio_mode(const char* mode, GpioMode* value) { + if(strcmp(mode, "input") == 0) { + *value = GpioModeInput; + return true; + } else if(strcmp(mode, "outputPushPull") == 0) { + *value = GpioModeOutputPushPull; + return true; + } else if(strcmp(mode, "outputOpenDrain") == 0) { + *value = GpioModeOutputOpenDrain; + return true; + } else if(strcmp(mode, "altFunctionPushPull") == 0) { + *value = GpioModeAltFunctionPushPull; + return true; + } else if(strcmp(mode, "altFunctionOpenDrain") == 0) { + *value = GpioModeAltFunctionOpenDrain; + return true; + } else if(strcmp(mode, "analog") == 0) { + *value = GpioModeAnalog; + return true; + } else if(strcmp(mode, "interruptRise") == 0) { + *value = GpioModeInterruptRise; + return true; + } else if(strcmp(mode, "interruptFall") == 0) { + *value = GpioModeInterruptFall; + return true; + } else if(strcmp(mode, "interruptRiseFall") == 0) { + *value = GpioModeInterruptRiseFall; + return true; + } else if(strcmp(mode, "eventRise") == 0) { + *value = GpioModeEventRise; + return true; + } else if(strcmp(mode, "eventFall") == 0) { + *value = GpioModeEventFall; + return true; + } else if(strcmp(mode, "eventRiseFall") == 0) { + *value = GpioModeEventRiseFall; + return true; + } else { + return false; + } +} + +const GpioPin* js_gpio_get_gpio_pin(const char* name) { + for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { + if(strcmp(js_gpio_pins[i].name, name) == 0) { + return js_gpio_pins[i].pin; + } + } + return NULL; +} + +static void js_gpio_init(struct mjs* mjs) { + mjs_val_t pin_arg = mjs_arg(mjs, 0); + mjs_val_t mode_arg = mjs_arg(mjs, 1); + mjs_val_t pull_arg = mjs_arg(mjs, 2); + + if(!mjs_is_string(pin_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); + if(!pin_name) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_string(mode_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const char* mode_name = mjs_get_string(mjs, &mode_arg, NULL); + if(!mode_name) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get mode name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_string(pull_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const char* pull_name = mjs_get_string(mjs, &pull_arg, NULL); + if(!pull_name) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pull name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); + if(gpio_pin == NULL) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + GpioMode gpio_mode; + if(!js_gpio_get_gpio_mode(mode_name, &gpio_mode)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid mode name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + GpioPull gpio_pull; + if(!js_gpio_get_gpio_pull(pull_name, &gpio_pull)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pull name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + expansion_disable(furi_record_open(RECORD_EXPANSION)); + furi_record_close(RECORD_EXPANSION); + + furi_hal_gpio_init(gpio_pin, gpio_mode, gpio_pull, GpioSpeedVeryHigh); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_gpio_write(struct mjs* mjs) { + mjs_val_t pin_arg = mjs_arg(mjs, 0); + mjs_val_t value_arg = mjs_arg(mjs, 1); + + if(!mjs_is_string(pin_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); + if(!pin_name) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + if(!mjs_is_boolean(value_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a boolean"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + bool value = mjs_get_bool(mjs, value_arg); + + const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); + + if(gpio_pin == NULL) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + furi_hal_gpio_write(gpio_pin, value); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_gpio_read(struct mjs* mjs) { + mjs_val_t pin_arg = mjs_arg(mjs, 0); + + if(!mjs_is_string(pin_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); + if(!pin_name) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); + + if(gpio_pin == NULL) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + bool value = furi_hal_gpio_read(gpio_pin); + + mjs_return(mjs, mjs_mk_boolean(mjs, value)); +} + +static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object) { + mjs_val_t gpio_obj = mjs_mk_object(mjs); + mjs_set(mjs, gpio_obj, "init", ~0, MJS_MK_FN(js_gpio_init)); + mjs_set(mjs, gpio_obj, "write", ~0, MJS_MK_FN(js_gpio_write)); + mjs_set(mjs, gpio_obj, "read", ~0, MJS_MK_FN(js_gpio_read)); + *object = gpio_obj; + + return (void*)1; +} + +static void js_gpio_destroy(void* inst) { + UNUSED(inst); + + // loop through all pins and reset them to analog mode + for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { + furi_hal_gpio_write(js_gpio_pins[i].pin, false); + furi_hal_gpio_init(js_gpio_pins[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh); + } + + expansion_enable(furi_record_open(RECORD_EXPANSION)); + furi_record_close(RECORD_EXPANSION); +} + +static const JsModuleDescriptor js_gpio_desc = { + "gpio", + js_gpio_create, + js_gpio_destroy, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_gpio_desc, +}; + +const FlipperAppPluginDescriptor* js_gpio_ep(void) { + return &plugin_descriptor; +} \ No newline at end of file