diff --git a/.gitignore b/.gitignore index c97ba8da9..84b8e8319 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,7 @@ PVS-Studio.log .gdbinit -/fbt_options_local.py \ No newline at end of file +/fbt_options_local.py + +# JS packages +node_modules/ diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js index 0927595a2..a08041e9f 100644 --- a/applications/debug/unit_tests/resources/unit_tests/js/basic.js +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -1,4 +1,15 @@ let tests = require("tests"); +let flipper = require("flipper"); tests.assert_eq(1337, 1337); tests.assert_eq("hello", "hello"); + +tests.assert_eq("compatible", sdkCompatibilityStatus(0, 1)); +tests.assert_eq("firmwareTooOld", sdkCompatibilityStatus(100500, 0)); +tests.assert_eq("firmwareTooNew", sdkCompatibilityStatus(-100500, 0)); +tests.assert_eq(true, doesSdkSupport(["baseline"])); +tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"])); + +tests.assert_eq("flipperdevices", flipper.firmwareVendor); +tests.assert_eq(0, flipper.jsSdkVersion[0]); +tests.assert_eq(1, flipper.jsSdkVersion[1]); diff --git a/applications/system/application.fam b/applications/system/application.fam index c5f81defa..9a7ae40b1 100644 --- a/applications/system/application.fam +++ b/applications/system/application.fam @@ -5,7 +5,6 @@ App( provides=[ "updater_app", "js_app", - "js_app_start", # "archive", ], ) diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 36fd7b16c..d9b5c8f20 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -6,6 +6,16 @@ App( stack_size=2 * 1024, resources="examples", order=0, + provides=["js_app_start"], + sources=[ + "js_app.c", + "js_modules.c", + "js_thread.c", + "plugin_api/app_api_table.cpp", + "views/console_view.c", + "modules/js_flipper.c", + "modules/js_tests.c", + ], ) App( @@ -13,6 +23,7 @@ App( apptype=FlipperAppType.STARTUP, entry_point="js_app_on_system_start", order=160, + sources=["js_app.c"], ) App( diff --git a/applications/system/js_app/examples/apps/Scripts/gpio.js b/applications/system/js_app/examples/apps/Scripts/gpio.js index f3b4bc121..24d0f0286 100644 --- a/applications/system/js_app/examples/apps/Scripts/gpio.js +++ b/applications/system/js_app/examples/apps/Scripts/gpio.js @@ -19,7 +19,7 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, // read potentiometer when button is pressed print("Press the button (PC1)"); eventLoop.subscribe(button.interrupt(), function (_, _item, pot) { - print("PC0 is at", pot.read_analog(), "mV"); + print("PC0 is at", pot.readAnalog(), "mV"); }, pot); // the program will just exit unless this is here diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 38ff46f75..bffa553a8 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -1,6 +1,8 @@ #include #include "js_modules.h" #include +#include +#include #include "modules/js_flipper.h" #ifdef FW_CFG_unit_tests @@ -76,6 +78,12 @@ JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) { } mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) { + // Ignore the initial part of the module name + const char* optional_module_prefix = "@" JS_SDK_VENDOR "/fz-sdk/"; + if(strncmp(name, optional_module_prefix, strlen(optional_module_prefix)) == 0) { + name += strlen(optional_module_prefix); + } + // Check if module is already installed JsModuleData* module_inst = js_find_loaded_module(modules, name); if(module_inst) { //-V547 @@ -175,3 +183,133 @@ void* js_module_get(JsModules* modules, const char* name) { furi_string_free(module_name); return module_inst ? module_inst->context : NULL; } + +typedef enum { + JsSdkCompatStatusCompatible, + JsSdkCompatStatusFirmwareTooOld, + JsSdkCompatStatusFirmwareTooNew, +} JsSdkCompatStatus; + +/** + * @brief Checks compatibility between the firmware and the JS SDK version + * expected by the script + */ +static JsSdkCompatStatus + js_internal_sdk_compatibility_status(int32_t exp_major, int32_t exp_minor) { + if(exp_major < JS_SDK_MAJOR) return JsSdkCompatStatusFirmwareTooNew; + if(exp_major > JS_SDK_MAJOR || exp_minor > JS_SDK_MINOR) + return JsSdkCompatStatusFirmwareTooOld; + return JsSdkCompatStatusCompatible; +} + +#define JS_SDK_COMPAT_ARGS \ + int32_t major, minor; \ + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&major), JS_ARG_INT32(&minor)); + +void js_sdk_compatibility_status(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + switch(status) { + case JsSdkCompatStatusCompatible: + mjs_return(mjs, mjs_mk_string(mjs, "compatible", ~0, 0)); + return; + case JsSdkCompatStatusFirmwareTooOld: + mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooOld", ~0, 0)); + return; + case JsSdkCompatStatusFirmwareTooNew: + mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooNew", ~0, 0)); + return; + } +} + +void js_is_sdk_compatible(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible)); +} + +/** + * @brief Asks the user whether to continue executing an incompatible script + */ +static bool js_internal_compat_ask_user(const char* message) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* dialog = dialog_message_alloc(); + dialog_message_set_header(dialog, message, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + dialog, "This script may not\nwork as expected", 79, 32, AlignCenter, AlignCenter); + dialog_message_set_icon(dialog, &I_Warning_30x23, 0, 18); + dialog_message_set_buttons(dialog, "Go back", NULL, "Run anyway"); + DialogMessageButton choice = dialog_message_show(dialogs, dialog); + dialog_message_free(dialog); + furi_record_close(RECORD_DIALOGS); + return choice == DialogMessageButtonRight; +} + +void js_check_sdk_compatibility(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + if(status != JsSdkCompatStatusCompatible) { + FURI_LOG_E( + TAG, + "Script requests JS SDK %ld.%ld, firmware provides JS SDK %d.%d", + major, + minor, + JS_SDK_MAJOR, + JS_SDK_MINOR); + + const char* message = (status == JsSdkCompatStatusFirmwareTooOld) ? "Outdated Firmware" : + "Outdated Script"; + if(!js_internal_compat_ask_user(message)) { + JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script"); + } + } +} + +static const char* extra_features[] = { + "baseline", // dummy "feature" +}; + +/** + * @brief Determines whether a feature is supported + */ +static bool js_internal_supports(const char* feature) { + for(size_t i = 0; i < COUNT_OF(extra_features); i++) { // -V1008 + if(strcmp(feature, extra_features[i]) == 0) return true; + } + return false; +} + +/** + * @brief Determines whether all of the requested features are supported + */ +static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) { + furi_assert(mjs_is_array(feature_arr)); + + for(size_t i = 0; i < mjs_array_length(mjs, feature_arr); i++) { + mjs_val_t feature = mjs_array_get(mjs, feature_arr, i); + const char* feature_str = mjs_get_string(mjs, &feature, NULL); + if(!feature_str) return false; + + if(!js_internal_supports(feature_str)) return false; + } + + return true; +} + +void js_does_sdk_support(struct mjs* mjs) { + mjs_val_t features; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features))); +} + +void js_check_sdk_features(struct mjs* mjs) { + mjs_val_t features; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + if(!js_internal_supports_all_of(mjs, features)) { + FURI_LOG_E(TAG, "Script requests unsupported features"); + + if(!js_internal_compat_ask_user("Unsupported Feature")) { + JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script"); + } + } +} diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 788715872..1dfd59521 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -9,6 +9,10 @@ #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 +#define JS_SDK_VENDOR "flipperdevices" +#define JS_SDK_MAJOR 0 +#define JS_SDK_MINOR 1 + /** * @brief Returns the foreign pointer in `obj["_"]` */ @@ -275,3 +279,28 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le * @returns Pointer to module context, NULL if the module is not instantiated */ void* js_module_get(JsModules* modules, const char* name); + +/** + * @brief `sdkCompatibilityStatus` function + */ +void js_sdk_compatibility_status(struct mjs* mjs); + +/** + * @brief `isSdkCompatible` function + */ +void js_is_sdk_compatible(struct mjs* mjs); + +/** + * @brief `checkSdkCompatibility` function + */ +void js_check_sdk_compatibility(struct mjs* mjs); + +/** + * @brief `doesSdkSupport` function + */ +void js_does_sdk_support(struct mjs* mjs); + +/** + * @brief `checkSdkFeatures` function + */ +void js_check_sdk_features(struct mjs* mjs); diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 7e7280e9c..0f3ecb96e 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -231,18 +231,29 @@ static int32_t js_thread(void* arg) { struct mjs* mjs = mjs_create(worker); worker->modules = js_modules_create(mjs, worker->resolver); mjs_val_t global = mjs_get_global(mjs); - mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); - mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); - mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string)); - mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); - mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); - mjs_val_t console_obj = mjs_mk_object(mjs); - mjs_set(mjs, console_obj, "log", ~0, MJS_MK_FN(js_console_log)); - mjs_set(mjs, console_obj, "warn", ~0, MJS_MK_FN(js_console_warn)); - mjs_set(mjs, console_obj, "error", ~0, MJS_MK_FN(js_console_error)); - mjs_set(mjs, console_obj, "debug", ~0, MJS_MK_FN(js_console_debug)); - mjs_set(mjs, global, "console", ~0, console_obj); + + JS_ASSIGN_MULTI(mjs, global) { + JS_FIELD("print", MJS_MK_FN(js_print)); + JS_FIELD("delay", MJS_MK_FN(js_delay)); + JS_FIELD("toString", MJS_MK_FN(js_global_to_string)); + JS_FIELD("ffi_address", MJS_MK_FN(js_ffi_address)); + JS_FIELD("require", MJS_MK_FN(js_require)); + JS_FIELD("console", console_obj); + + JS_FIELD("sdkCompatibilityStatus", MJS_MK_FN(js_sdk_compatibility_status)); + JS_FIELD("isSdkCompatible", MJS_MK_FN(js_is_sdk_compatible)); + JS_FIELD("checkSdkCompatibility", MJS_MK_FN(js_check_sdk_compatibility)); + JS_FIELD("doesSdkSupport", MJS_MK_FN(js_does_sdk_support)); + JS_FIELD("checkSdkFeatures", MJS_MK_FN(js_check_sdk_features)); + } + + JS_ASSIGN_MULTI(mjs, console_obj) { + JS_FIELD("log", MJS_MK_FN(js_console_log)); + JS_FIELD("warn", MJS_MK_FN(js_console_warn)); + JS_FIELD("error", MJS_MK_FN(js_console_error)); + JS_FIELD("debug", MJS_MK_FN(js_console_debug)); + } mjs_set_ffi_resolver(mjs, js_dlsym, worker->resolver); diff --git a/applications/system/js_app/modules/js_flipper.c b/applications/system/js_app/modules/js_flipper.c index 43c675e10..eeaa2c8a0 100644 --- a/applications/system/js_app/modules/js_flipper.c +++ b/applications/system/js_app/modules/js_flipper.c @@ -27,11 +27,19 @@ static void js_flipper_get_battery(struct mjs* mjs) { void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); + mjs_val_t sdk_vsn = mjs_mk_array(mjs); + mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MAJOR)); + mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MINOR)); + mjs_val_t flipper_obj = mjs_mk_object(mjs); - mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model)); - mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name)); - mjs_set(mjs, flipper_obj, "getBatteryCharge", ~0, MJS_MK_FN(js_flipper_get_battery)); *object = flipper_obj; + JS_ASSIGN_MULTI(mjs, flipper_obj) { + JS_FIELD("getModel", MJS_MK_FN(js_flipper_get_model)); + JS_FIELD("getName", MJS_MK_FN(js_flipper_get_name)); + JS_FIELD("getBatteryCharge", MJS_MK_FN(js_flipper_get_battery)); + JS_FIELD("firmwareVendor", mjs_mk_string(mjs, JS_SDK_VENDOR, ~0, false)); + JS_FIELD("jsSdkVersion", sdk_vsn); + } return (void*)1; } diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 70021968f..f3cda7ece 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -220,7 +220,7 @@ static void js_gpio_interrupt(struct mjs* mjs) { * let gpio = require("gpio"); * let pot = gpio.get("pc0"); * pot.init({ direction: "in", inMode: "analog" }); - * print("voltage:" pot.read_analog(), "mV"); + * print("voltage:" pot.readAnalog(), "mV"); * ``` */ static void js_gpio_read_analog(struct mjs* mjs) { @@ -274,7 +274,7 @@ static void js_gpio_get(struct mjs* mjs) { mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); - mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog)); + mjs_set(mjs, manager, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); mjs_return(mjs, manager); diff --git a/applications/system/js_app/packages/create-fz-app/README.md b/applications/system/js_app/packages/create-fz-app/README.md new file mode 100644 index 000000000..cf6ddbc91 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/README.md @@ -0,0 +1,20 @@ +# Flipper Zero JavaScript SDK Wizard +This package contains an interactive wizard that lets you scaffold a JavaScript +application for Flipper Zero. + +## Getting started +Create your application using the interactive wizard: +```shell +npx @flipperdevices/create-fz-app@latest +``` + +Then, enter the directory with your application and launch it: +```shell +cd my-flip-app +npm start +``` + +You are free to use `pnpm` or `yarn` instead of `npm`. + +## Documentation +Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html) diff --git a/applications/system/js_app/packages/create-fz-app/index.js b/applications/system/js_app/packages/create-fz-app/index.js new file mode 100755 index 000000000..0bfe9376e --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/index.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +import prompts from "prompts"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "url"; +import { spawnSync } from "node:child_process"; +import { replaceInFileSync } from "replace-in-file"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +(async () => { + const { name, pkgManager, confirm } = await prompts([ + { + type: "text", + name: "name", + message: "What is the name of your project?", + initial: "my-flip-app" + }, + { + type: "select", + name: "pkgManager", + message: "What package manager should your project use?", + choices: [ + { title: "npm", value: "npm" }, + { title: "pnpm", value: "pnpm" }, + { title: "yarn", value: "yarn" }, + ], + }, + { + type: "confirm", + name: "confirm", + message: "Create project?", + initial: true, + }, + ]); + + if (!confirm) + return; + + if (fs.existsSync(name)) { + const { replace } = await prompts([ + { + type: "confirm", + name: "replace", + message: `File or directory \`${name}\` already exists. Continue anyway?`, + initial: false, + }, + ]); + if (!replace) + return; + } + + fs.rmSync(name, { recursive: true, force: true }); + + console.log("Copying files..."); + fs.cpSync(path.resolve(__dirname, "template"), name, { recursive: true }); + replaceInFileSync({ files: `${name}/**/*`, from: //g, to: name }); + + console.log("Installing packages..."); + spawnSync("bash", ["-c", `cd ${name} && ${pkgManager} install`], { + cwd: process.cwd(), + detached: true, + stdio: "inherit", + }); + + console.log(`Done! Created ${name}. Run \`cd ${name} && ${pkgManager} start\` to run it on your Flipper.`); +})(); diff --git a/applications/system/js_app/packages/create-fz-app/package.json b/applications/system/js_app/packages/create-fz-app/package.json new file mode 100644 index 000000000..216423396 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "@flipperdevices/create-fz-app", + "version": "0.1.0", + "description": "Template package for JS apps Flipper Zero", + "bin": "index.js", + "type": "module", + "keywords": [ + "flipper", + "flipper zero" + ], + "author": "Flipper Devices", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git+https://github.com/flipperdevices/flipperzero-firmware.git", + "directory": "applications/system/js_app/packages/create-fz-app" + }, + "dependencies": { + "prompts": "^2.4.2", + "replace-in-file": "^8.2.0" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml b/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml new file mode 100644 index 000000000..58f20a385 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml @@ -0,0 +1,373 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 + replace-in-file: + specifier: ^8.2.0 + version: 8.2.0 + +packages: + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + replace-in-file@8.2.0: + resolution: {integrity: sha512-hMsQtdYHwWviQT5ZbNsgfu0WuCiNlcUSnnD+aHAL081kbU9dPkPocDaHlDvAHKydTWWpx1apfcEcmvIyQk3CpQ==} + engines: {node: '>=18'} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + chalk@5.3.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + escalade@3.2.0: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + get-caller-file@2.0.5: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + kleur@3.0.3: {} + + lru-cache@10.4.3: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + replace-in-file@8.2.0: + dependencies: + chalk: 5.3.0 + glob: 10.4.5 + yargs: 17.7.2 + + require-directory@2.1.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/applications/system/js_app/packages/create-fz-app/template/.gitignore b/applications/system/js_app/packages/create-fz-app/template/.gitignore new file mode 100644 index 000000000..aa57f8d03 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/.gitignore @@ -0,0 +1,2 @@ +/dist +node_modules/ diff --git a/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 b/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 new file mode 100644 index 000000000..e545841c5 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 @@ -0,0 +1,23 @@ +{ + build: { + // Where to put the compiled file + output: "dist/.js", + + // Whether to reduce the final file size at the cost of readability and + // clarity of error messages + minify: false, + + // Set this to `false` if you've thoroughly read the documentation and + // are sure that you can use manual version checks to your advantage + enforceSdkVersion: true, + }, + + upload: { + // Where to grab the file from. If you're not doing any extra processing + // after the SDK, this should match `build.output` + input: "dist/.js", + + // Where to put the file on the device + output: "/ext/apps/Scripts/.js", + }, +} diff --git a/applications/system/js_app/packages/create-fz-app/template/index.ts b/applications/system/js_app/packages/create-fz-app/template/index.ts new file mode 100644 index 000000000..6291e3e13 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/index.ts @@ -0,0 +1,30 @@ +// import modules +// caution: `eventLoop` HAS to be imported before `gui`, and `gui` HAS to be +// imported before any `gui` submodules. +import * as eventLoop from "@flipperdevices/fz-sdk/event_loop"; +import * as gui from "@flipperdevices/fz-sdk/gui"; +import * as dialog from "@flipperdevices/fz-sdk/gui/dialog"; + +// a common pattern is to declare all the views that your app uses on one object +const views = { + dialog: dialog.makeWith({ + header: "Hello from ", + text: "Check out index.ts and\nchange something :)", + center: "Gonna do that!", + }), +}; + +// stop app on center button press +eventLoop.subscribe(views.dialog.input, (_sub, button, eventLoop) => { + if (button === "center") + eventLoop.stop(); +}, eventLoop); + +// stop app on back button press +eventLoop.subscribe(gui.viewDispatcher.navigation, (_sub, _item, eventLoop) => { + eventLoop.stop(); +}, eventLoop); + +// run app +gui.viewDispatcher.switchTo(views.dialog); +eventLoop.run(); diff --git a/applications/system/js_app/packages/create-fz-app/template/package.json b/applications/system/js_app/packages/create-fz-app/template/package.json new file mode 100644 index 000000000..7acdeccaa --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/package.json @@ -0,0 +1,12 @@ +{ + "name": "", + "version": "1.0.0", + "scripts": { + "build": "tsc && node node_modules/@flipperdevices/fz-sdk/sdk.js build", + "start": "npm run build && node node_modules/@flipperdevices/fz-sdk/sdk.js upload" + }, + "devDependencies": { + "@flipperdevices/fz-sdk": "^0.1", + "typescript": "^5.6.3" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/create-fz-app/template/tsconfig.json b/applications/system/js_app/packages/create-fz-app/template/tsconfig.json new file mode 100644 index 000000000..c7b83cd5d --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "outDir": "dist", + "checkJs": true, + "module": "CommonJS", + "noLib": true, + "target": "ES2015", + }, + "files": [ + "./node_modules/@flipperdevices/fz-sdk/global.d.ts", + ], + "include": [ + "./**/*.ts", + "./**/*.js" + ], + "exclude": [ + "./node_modules/**/*", + "dist/**/*", + ], +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/.gitignore b/applications/system/js_app/packages/fz-sdk/.gitignore new file mode 100644 index 000000000..77f12ae2e --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/.gitignore @@ -0,0 +1 @@ +docs/ diff --git a/applications/system/js_app/packages/fz-sdk/README.md b/applications/system/js_app/packages/fz-sdk/README.md new file mode 100644 index 000000000..3234f68aa --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/README.md @@ -0,0 +1,31 @@ +# Flipper Zero JavaScript SDK +This package contains official tooling and typings for developing Flipper Zero +applications in JavaScript. + +## Getting started +Create your application using the interactive wizard: +```shell +npx @flipperdevices/create-fz-app@latest +``` + +Then, enter the directory with your application and launch it: +```shell +cd my-flip-app +npm start +``` + +You are free to use `pnpm` or `yarn` instead of `npm`. + +## Versioning +For each version of this package, the major and minor components match those of +the Flipper Zero JS SDK version that that package version targets. This version +follows semver. For example, apps compiled with SDK version `0.1.0` will be +compatible with SDK versions `0.1`...`1.0` (not including `1.0`). + +Every API has a version history reflected in its JSDoc comment. It is heavily +recommended to check SDK compatibility using a combination of +`sdkCompatibilityStatus`, `isSdkCompatible`, `assertSdkCompatibility` depending +on your use case. + +## Documentation +Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html) diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/packages/fz-sdk/badusb/index.d.ts similarity index 90% rename from applications/system/js_app/types/badusb/index.d.ts rename to applications/system/js_app/packages/fz-sdk/badusb/index.d.ts index 647382dc0..6da705495 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/badusb/index.d.ts @@ -1,8 +1,10 @@ /** * @brief Special key codes that this module recognizes + * @version Added in JS SDK 0.1 */ export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI"; +/** @version Added in JS SDK 0.1 */ export type MainKey = "DOWN" | "LEFT" | "RIGHT" | "UP" | @@ -28,16 +30,19 @@ export type MainKey = "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"; +/** @version Added in JS SDK 0.1 */ export type KeyCode = MainKey | ModifierKey | number; /** * @brief Initializes the module * @param settings USB device settings. Omit to select default parameters + * @version Added in JS SDK 0.1 */ export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void; /** * @brief Tells whether the virtual USB HID device has successfully connected + * @version Added in JS SDK 0.1 */ export declare function isConnected(): boolean; @@ -46,6 +51,7 @@ export declare function isConnected(): boolean; * @param keys The arguments represent a set of keys to. Out of that set, only * one of the keys may represent a "main key" (see `MainKey`), with * the rest being modifier keys (see `ModifierKey`). + * @version Added in JS SDK 0.1 */ export declare function press(...keys: KeyCode[]): void; @@ -54,6 +60,7 @@ export declare function press(...keys: KeyCode[]): void; * @param keys The arguments represent a set of keys to. Out of that set, only * one of the keys may represent a "main key" (see `MainKey`), with * the rest being modifier keys (see `ModifierKey`). + * @version Added in JS SDK 0.1 */ export declare function hold(...keys: KeyCode[]): void; @@ -62,6 +69,7 @@ export declare function hold(...keys: KeyCode[]): void; * @param keys The arguments represent a set of keys to. Out of that set, only * one of the keys may represent a "main key" (see `MainKey`), with * the rest being modifier keys (see `ModifierKey`). + * @version Added in JS SDK 0.1 */ export declare function release(...keys: KeyCode[]): void; @@ -69,6 +77,7 @@ export declare function release(...keys: KeyCode[]): void; * @brief Prints a string by repeatedly pressing and releasing keys * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function print(string: string, delay?: number): void; @@ -77,5 +86,6 @@ export declare function print(string: string, delay?: number): void; * "Enter" after printing the string * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function println(string: string, delay?: number): void; diff --git a/applications/system/js_app/packages/fz-sdk/docs_readme.md b/applications/system/js_app/packages/fz-sdk/docs_readme.md new file mode 100644 index 000000000..f82f58ec7 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/docs_readme.md @@ -0,0 +1 @@ +# Welcome diff --git a/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts b/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts new file mode 100644 index 000000000..001518f87 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts @@ -0,0 +1,182 @@ +/** + * Module for dealing with events + * + * ```js + * let eventLoop = require("event_loop"); + * ``` + * + * The event loop is central to event-based programming in many frameworks, and + * our JS subsystem is no exception. It is a good idea to familiarize yourself + * with the event loop first before using any of the advanced modules (e.g. GPIO + * and GUI). + * + * # Conceptualizing the event loop + * If you ever wrote JavaScript before, you have definitely seen callbacks. It's + * when a function accepts another function (usually an anonymous one) as one of + * the arguments, which it will call later on, e.g. when an event happens or + * when data becomes ready: + * ```js + * setTimeout(function() { console.log("Hello, World!") }, 1000); + * ``` + * + * Many JavaScript engines employ a queue that the runtime fetches events from + * as they occur, subsequently calling the corresponding callbacks. This is done + * in a long-running loop, hence the name "event loop". Here's the pseudocode + * for a typical event loop: + * ```js + * while(loop_is_running()) { + * if(event_available_in_queue()) { + * let event = fetch_event_from_queue(); + * let callback = get_callback_associated_with(event); + * if(callback) + * callback(get_extra_data_for(event)); + * } else { + * // avoid wasting CPU time + * sleep_until_any_event_becomes_available(); + * } + * } + * ``` + * + * Most JS runtimes enclose the event loop within themselves, so that most JS + * programmers does not even need to be aware of its existence. This is not the + * case with our JS subsystem. + * + * # Example + * This is how one would write something similar to the `setTimeout` example + * above: + * ```js + * // import module + * let eventLoop = require("event_loop"); + * + * // create an event source that will fire once 1 second after it has been created + * let timer = eventLoop.timer("oneshot", 1000); + * + * // subscribe a callback to the event source + * eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) { + * print("Hello, World!"); + * eventLoop.stop(); + * }, eventLoop); // notice this extra argument. we'll come back to this later + * + * // run the loop until it is stopped + * eventLoop.run(); + * + * // the previous line will only finish executing once `.stop()` is called, hence + * // the following line will execute only after "Hello, World!" is printed + * print("Stopped"); + * ``` + * + * I promised you that we'll come back to the extra argument after the callback + * function. Our JavaScript engine does not support closures (anonymous + * functions that access values outside of their arguments), so we ask + * `subscribe` to pass an outside value (namely, `eventLoop`) as an argument to + * the callback so that we can access it. We can modify this extra state: + * ```js + * // this timer will fire every second + * let timer = eventLoop.timer("periodic", 1000); + * eventLoop.subscribe(timer, function(_subscription, _item, counter, eventLoop) { + * print("Counter is at:", counter); + * if(counter === 10) + * eventLoop.stop(); + * // modify the extra arguments that will be passed to us the next time + * return [counter + 1, eventLoop]; + * }, 0, eventLoop); + * ``` + * + * Because we have two extra arguments, if we return anything other than an + * array of length 2, the arguments will be kept as-is for the next call. + * + * The first two arguments that get passed to our callback are: + * - The subscription manager that lets us `.cancel()` our subscription + * - The event item, used for events that have extra data. Timer events do + * not, they just produce `undefined`. + * + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @ignore + */ +type Lit = undefined | null | {}; + +/** + * Subscription control interface + * @version Added in JS SDK 0.1 + */ +export interface Subscription { + /** + * Cancels the subscription, preventing any future events managed by the + * subscription from firing + * @version Added in JS SDK 0.1 + */ + cancel(): void; +} + +/** + * Opaque event source identifier + * @version Added in JS SDK 0.1 + */ +export type Contract = symbol & { "__tag__": "contract" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * A callback can be assigned to an event loop to listen to an event. It may + * return an array with values that will be passed to it as arguments the next + * time that it is called. The first argument is always the subscription + * manager, and the second argument is always the item that trigged the event. + * The type of the item is defined by the event source. + * @version Added in JS SDK 0.1 + */ +export type Callback = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void; + +/** + * Subscribes a callback to an event + * @param contract Event identifier + * @param callback Function to call when the event is triggered + * @param args Initial arguments passed to the callback + * @version Added in JS SDK 0.1 + */ +export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; +/** + * Runs the event loop until it is stopped (potentially never) + * @version Added in JS SDK 0.1 + */ +export function run(): void | never; +/** + * Stops the event loop + * @version Added in JS SDK 0.1 + */ +export function stop(): void; + +/** + * Creates a timer event that can be subscribed to just like any other event + * @param mode Either `"oneshot"` or `"periodic"` + * @param interval Timer interval in milliseconds + * @version Added in JS SDK 0.1 + */ +export function timer(mode: "oneshot" | "periodic", interval: number): Contract; + +/** + * Message queue + * @version Added in JS SDK 0.1 + */ +export declare class Queue { + /** + * Message event + * @version Added in JS SDK 0.1 + */ + input: Contract; + /** + * Sends a message to the queue + * @param message message to send + * @version Added in JS SDK 0.1 + */ + send(message: T): void; +} + +/** + * Creates a message queue + * @param length maximum queue capacity + * @version Added in JS SDK 0.1 + */ +export function queue(length: number): Queue; diff --git a/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts b/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts new file mode 100644 index 000000000..2dac4204b --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts @@ -0,0 +1,41 @@ +/** + * Module for querying device properties + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @brief Returns the device model + * @version Added in JS SDK 0.1 + */ +export declare function getModel(): string; + +/** + * @brief Returns the name of the virtual dolphin + * @version Added in JS SDK 0.1 + */ +export declare function getName(): string; + +/** + * @brief Returns the battery charge percentage + * @version Added in JS SDK 0.1 + */ +export declare function getBatteryCharge(): number; + +/** + * @warning Do **NOT** use this to check the presence or absence of features. If + * you do, I'm gonna be sad :( Instead, refer to `checkSdkFeatures` and + * other similar mechanisms. + * @note Original firmware reports `"flipperdevices"`. + * @version Added in JS SDK 0.1 + */ +export declare const firmwareVendor: string; + +/** + * @warning Do **NOT** use this to check the presence or absence of features. If + * you do, I'm gonna be sad :( Instead, refer to + * `checkSdkCompatibility` and other similar mechanisms. + * @note You're looking at JS SDK 0.1 + * @version Added in JS SDK 0.1 + */ +export declare const jsSdkVersion: [number, number]; diff --git a/applications/system/js_app/packages/fz-sdk/global.d.ts b/applications/system/js_app/packages/fz-sdk/global.d.ts new file mode 100644 index 000000000..0c40f0c48 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/global.d.ts @@ -0,0 +1,344 @@ +/** + * Things from this module are automatically available to you without having to + * explicitly import anything. + * + * # SDK versioning and features + * + * ## Motivation + * It is very important that you check that features are implemented before you + * use them. By adding the necessary checks, you ensure that your users get a + * clear warning instead of a cryptic error message when running the script. + * + * This system has been designed in collaboration with our community in order to + * make things better for everybody involved. You can find out more in this + * discussion: https://github.com/flipperdevices/flipperzero-firmware/pull/3961 + * + * ## Community agreement + * Each interpreter implementation (aka "JS SDK", aka "JS API"), including + * those found in third-party firmware distributions, defines two markers for + * signaling what it supports: the **SDK version** and the + * **extra feature set**. + * + * The **SDK version** consists of two semver-like integer components: the major + * version and the minor version. Like semver, the major version is bumped when + * a breaking change is introduced (i.e. one that would require correction of + * apps by their developers), and the minor version is bumped when a new + * non-breaking feature is introduced. Because we have adopted TypeScript, + * the https://www.semver-ts.org/ standard is used to determine whether a change + * is breaking or not. The basis of `semver-ts` is the "no new red squiggles" + * rule. + * + * Every major version is associated with a set of **extra features** that are + * present in some firmware distributions but not others. Distributions may + * cross-port features between each other, until at some point they get ported + * into the upstream firmware distribution. With the next major release of the + * JS SDK, all extra features present in the upstream distribution are now + * declared **baseline features**, and thus no longer recognized as "extra + * features". + * + * Before using a feature, you must check that the interpreter that you're + * running on actually supports it. If you don't, the portability of your + * application will suffer. + * + * ## Implementation + * Use the following functions to check version compatibility: + * - `checkSdkCompatibility` when your script absolutely cannot function on an + * incompatible interpreter + * - `isSdkCompatible` when your script can leverage multiple interpreter + * editions to its advantage + * - `sdkCompatibilityStatus` when you need a detailed status on compatibility + * + * Use the following functions to check feature compatibility: + * - `checkSdkFeatures` when your script absolutely cannot function on an + * incompatible interpreter + * - `doesSdkSupport` when your script can leverage multiple interpreter + * editions to its advantage + * + * ## Automatic version enforcement + * The SDK will automatically insert a call to `checkSdkCompatibility` in the + * beginning of the resulting script. If you would like to disable this check + * and instead use other manual compatibility checking facilities, edit your + * `fz-sdk.config.json5`. + * + * # Standard library + * Standard library features are mostly unimplemented. This module defines, + * among other things, the features that _are_ implemented. + * + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @brief Checks compatibility between the script and the JS SDK that the + * firmware provides + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @returns Compatibility status: + * - `"compatible"` if the script and the JS SDK are compatible + * - `"firmwareTooOld"` if the expected major version is larger than the + * version of the firmware, or if the expected minor version is larger than + * the version of the firmware + * - `"firmwareTooNew"` if the expected major version is lower than the + * version of the firmware + * @version Added in JS SDK 0.1 + */ +declare function sdkCompatibilityStatus(expectedMajor: number, expectedMinor: number): + "compatible" | "firmwareTooOld" | "firmwareTooNew"; + +/** + * @brief Checks compatibility between the script and the JS SDK that the + * firmware provides in a boolean fashion + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @returns `true` if the two are compatible, `false` otherwise + * @version Added in JS SDK 0.1 + */ +declare function isSdkCompatible(expectedMajor: number, expectedMinor: number): boolean; + +/** + * @brief Asks the user whether to continue executing the script if the versions + * are not compatible. Does nothing if they are. + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @version Added in JS SDK 0.1 + */ +declare function checkSdkCompatibility(expectedMajor: number, expectedMinor: number): void | never; + +/** + * @brief Checks whether all of the specified extra features are supported by + * the interpreter. + * @warning This function will return `false` if a queried feature is now + * recognized as a baseline feature. For more info, consult the module + * documentation. + * @param features Array of named features to query + */ +declare function doesSdkSupport(features: string[]): boolean; + +/** + * @brief Checks whether all of the specified extra features are supported by + * the interpreter, asking the user if they want to continue running the + * script if they're not. + * @warning This function will act as if the feature is not implemented for + * features that are now recognized as baseline features. For more + * info, consult the module documentation. + * @param features Array of named features to query + */ +declare function checkSdkFeatures(features: string[]): void | never; + +/** + * @brief Pauses JavaScript execution for a while + * @param ms How many milliseconds to pause the execution for + * @version Added in JS SDK 0.1 + */ +declare function delay(ms: number): void; + +/** + * @brief Prints to the GUI console view + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the console view + * @version Added in JS SDK 0.1 + */ +declare function print(...args: any[]): void; + +/** + * @brief Converts a number to a string + * @param value The number to convert to a string + * @param base Integer base (`2`...`16`), default: 10 + * @version Added in JS SDK 0.1 + */ +declare function toString(value: number, base?: number): string; + +/** + * @brief Reads a JS value from a file + * + * Reads a file at the specified path, interprets it as a JS value and returns + * said value. + * + * @param path The path to the file + * @version Added in JS SDK 0.1 + */ +declare function load(path: string): any; + +/** + * @brief Loads a natively implemented module + * @param module The name of the module to load + * @version Added in JS SDK 0.1 + */ +declare function require(module: string): any; + +/** + * @brief mJS Foreign Pointer type + * + * JavaScript code cannot do anything with values of `RawPointer` type except + * acquire them from native code and pass them right back to other parts of + * native code. These values cannot be turned into something meaningful, nor can + * be they modified. + * + * @version Added in JS SDK 0.1 + */ +declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * @brief Holds raw bytes + * @version Added in JS SDK 0.1 + */ +declare class ArrayBuffer { + /** + * @brief The pointer to the byte buffer + * @note Like other `RawPointer` values, this value is essentially useless + * to JS code. + * @version Added in JS SDK 0.1 + */ + getPtr: RawPointer; + /** + * @brief The length of the buffer in bytes + * @version Added in JS SDK 0.1 + */ + byteLength: number; + /** + * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer + * @param start The index of the byte in the source buffer to be used as the + * start for the new buffer + * @param end The index of the byte in the source buffer that follows the + * byte to be used as the last byte for the new buffer + * @version Added in JS SDK 0.1 + */ + slice(start: number, end?: number): ArrayBuffer; +} + +declare function ArrayBuffer(): ArrayBuffer; + +declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; + +declare class TypedArray { + /** + * @brief The length of the buffer in bytes + * @version Added in JS SDK 0.1 + */ + byteLength: number; + /** + * @brief The length of the buffer in typed elements + * @version Added in JS SDK 0.1 + */ + length: number; + /** + * @brief The underlying `ArrayBuffer` + * @version Added in JS SDK 0.1 + */ + buffer: ArrayBuffer; +} + +declare class Uint8Array extends TypedArray<"u8"> { } +declare class Int8Array extends TypedArray<"i8"> { } +declare class Uint16Array extends TypedArray<"u16"> { } +declare class Int16Array extends TypedArray<"i16"> { } +declare class Uint32Array extends TypedArray<"u32"> { } +declare class Int32Array extends TypedArray<"i32"> { } + +declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; +declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; +declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; +declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; +declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; +declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; + +declare const console: { + /** + * @brief Prints to the UART logs at the `[I]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + log(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[D]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + debug(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[W]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + warn(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[E]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + * @version Added in JS SDK 0.1 + */ + error(...args: any[]): void; +}; + +declare class Array { + /** + * @brief Takes items out of the array + * + * Removes elements from the array and returns them in a new array + * + * @param start The index to start taking elements from + * @param deleteCount How many elements to take + * @returns The elements that were taken out of the original array as a new + * array + * @version Added in JS SDK 0.1 + */ + splice(start: number, deleteCount: number): T[]; + /** + * @brief Adds a value to the end of the array + * @param value The value to add + * @returns New length of the array + * @version Added in JS SDK 0.1 + */ + push(value: T): number; + /** + * @brief How many elements there are in the array + * @version Added in JS SDK 0.1 + */ + length: number; +} + +declare class String { + /** + * @brief How many characters there are in the string + * @version Added in JS SDK 0.1 + */ + length: number; + /** + * @brief Returns the character code at an index in the string + * @param index The index to consult + * @version Added in JS SDK 0.1 + */ + charCodeAt(index: number): number; + /** + * See `charCodeAt` + * @version Added in JS SDK 0.1 + */ + at(index: number): number; +} + +declare class Boolean { } + +declare class Function { } + +declare class Number { } + +declare class Object { } + +declare class RegExp { } + +declare interface IArguments { } + +declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gpio/index.d.ts b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts similarity index 59% rename from applications/system/js_app/types/gpio/index.d.ts rename to applications/system/js_app/packages/fz-sdk/gpio/index.d.ts index 18705f898..b484ebbf6 100644 --- a/applications/system/js_app/types/gpio/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts @@ -1,5 +1,37 @@ +/** + * Module for accessing the GPIO (General Purpose Input/Output) ports + * + * ```js + * let eventLoop = require("event_loop"); + * let gpio = require("gpio"); + * ``` + * + * This module depends on the `event_loop` module, so it _must_ only be imported + * after `event_loop` is imported. + * + * # Example + * ```js + * let eventLoop = require("event_loop"); + * let gpio = require("gpio"); + * + * let led = gpio.get("pc3"); + * led.init({ direction: "out", outMode: "push_pull" }); + * + * led.write(true); + * delay(1000); + * led.write(false); + * delay(1000); + * ``` + * + * @version Added in JS SDK 0.1 + * @module + */ + import type { Contract } from "../event_loop"; +/** + * @version Added in JS SDK 0.1 + */ export interface Mode { direction: "in" | "out"; outMode?: "push_pull" | "open_drain"; @@ -8,31 +40,39 @@ export interface Mode { pull?: "up" | "down"; } +/** + * @version Added in JS SDK 0.1 + */ export interface Pin { /** * Configures a pin. This may be done several times. * @param mode Pin configuration object + * @version Added in JS SDK 0.1 */ init(mode: Mode): void; /** * Sets the output value of a pin if it's been configured with * `direction: "out"`. * @param value Logic value to output + * @version Added in JS SDK 0.1 */ write(value: boolean): void; /** * Gets the input value of a pin if it's been configured with * `direction: "in"`, but not `inMode: "analog"`. + * @version Added in JS SDK 0.1 */ read(): boolean; /** * Gets the input voltage of a pin in millivolts if it's been configured * with `direction: "in"` and `inMode: "analog"` + * @version Added in JS SDK 0.1 */ - read_analog(): number; + readAnalog(): number; /** * Returns an `event_loop` event that can be used to listen to interrupts, * as configured by `init` + * @version Added in JS SDK 0.1 */ interrupt(): Contract; } @@ -41,5 +81,6 @@ export interface Pin { * Returns an object that can be used to manage a GPIO pin. For the list of * available pins, see https://docs.flipper.net/gpio-and-modules#miFsS * @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`) + * @version Added in JS SDK 0.1 */ export function get(pin: string | number): Pin; diff --git a/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts new file mode 100644 index 000000000..9bd0c3966 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts @@ -0,0 +1,45 @@ +/** + * Displays a dialog with up to three options. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let dialogView = require("gui/dialog"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed in bold at the top of the screen + * - `text`: Text displayed in the middle of the string + * - `left`: Text for the left button + * - `center`: Text for the center button + * - `right`: Text for the right button + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + text: string, + left: string, + center: string, + right: string, +} +declare class Dialog extends View { + input: Contract<"left" | "center" | "right">; +} +declare class DialogFactory extends ViewFactory { } +declare const factory: DialogFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts new file mode 100644 index 000000000..49e591426 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts @@ -0,0 +1,32 @@ +/** + * Displays nothing. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let emptyView = require("gui/empty_screen"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * This view does not have any props. + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class EmptyScreen extends View { } +declare class EmptyScreenFactory extends ViewFactory { } +declare const factory: EmptyScreenFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts new file mode 100644 index 000000000..3184a5718 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts @@ -0,0 +1,171 @@ +/** + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * ``` + * + * This module depends on the `event_loop` module, so it _must_ only be imported + * after `event_loop` is imported. + * + * ## Conceptualizing GUI + * ### Event loop + * It is highly recommended to familiarize yourself with the event loop first + * before doing GUI-related things. + * + * ### Canvas + * The canvas is just a drawing area with no abstractions over it. Drawing on + * the canvas directly (i.e. not through a viewport) is useful in case you want + * to implement a custom design element, but this is rather uncommon. + * + * ### Viewport + * A viewport is a window into a rectangular portion of the canvas. Applications + * always access the canvas through a viewport. + * + * ### View + * In Flipper's terminology, a "View" is a fullscreen design element that + * assumes control over the entire viewport and all input events. Different + * types of views are available (not all of which are unfortunately currently + * implemented in JS): + * | View | Has JS adapter? | + * |----------------------|------------------| + * | `button_menu` | ❌ | + * | `button_panel` | ❌ | + * | `byte_input` | ❌ | + * | `dialog_ex` | ✅ (as `dialog`) | + * | `empty_screen` | ✅ | + * | `file_browser` | ❌ | + * | `loading` | ✅ | + * | `menu` | ❌ | + * | `number_input` | ❌ | + * | `popup` | ❌ | + * | `submenu` | ✅ | + * | `text_box` | ✅ | + * | `text_input` | ✅ | + * | `variable_item_list` | ❌ | + * | `widget` | ❌ | + * + * In JS, each view has its own set of properties (or just "props"). The + * programmer can manipulate these properties in two ways: + * - Instantiate a `View` using the `makeWith(props)` method, passing an + * object with the initial properties + * - Call `set(name, value)` to modify a property of an existing `View` + * + * ### View Dispatcher + * The view dispatcher holds references to all the views that an application + * needs and switches between them as the application makes requests to do so. + * + * ### Scene Manager + * The scene manager is an optional add-on to the view dispatcher that makes + * managing applications with complex navigation flows easier. It is currently + * inaccessible from JS. + * + * ### Approaches + * In total, there are three different approaches that you may take when writing + * a GUI application: + * | Approach | Use cases | Available from JS | + * |----------------|------------------------------------------------------------------------------|-------------------| + * | ViewPort only | Accessing the graphics API directly, without any of the nice UI abstractions | ❌ | + * | ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ | + * | SceneManager | Additional navigation flow management for complex applications | ❌ | + * + * # Example + * An example with three different views using the ViewDispatcher approach: + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let loadingView = require("gui/loading"); + * let submenuView = require("gui/submenu"); + * let emptyView = require("gui/empty_screen"); + * + * // Common pattern: declare all the views in an object. This is absolutely not + * // required, but adds clarity to the script. + * let views = { + * // the view dispatcher auto-✨magically✨ remembers views as they are created + * loading: loadingView.make(), + * empty: emptyView.make(), + * demos: submenuView.makeWith({ + * items: [ + * "Hourglass screen", + * "Empty screen", + * "Exit app", + * ], + * }), + * }; + * + * // go to different screens depending on what was selected + * eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) { + * if (index === 0) { + * gui.viewDispatcher.switchTo(views.loading); + * } else if (index === 1) { + * gui.viewDispatcher.switchTo(views.empty); + * } else if (index === 2) { + * eventLoop.stop(); + * } + * }, gui, eventLoop, views); + * + * // go to the demo chooser screen when the back key is pressed + * eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) { + * gui.viewDispatcher.switchTo(views.demos); + * }, gui, views); + * + * // run UI + * gui.viewDispatcher.switchTo(views.demos); + * eventLoop.run(); + * ``` + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { Contract } from "../event_loop"; + +type Properties = { [K: string]: any }; + +export declare class View { + set

(property: P, value: Props[P]): void; +} + +export declare class ViewFactory> { + make(): V; + makeWith(initial: Partial): V; +} + +/** + * @version Added in JS SDK 0.1 + */ +declare class ViewDispatcher { + /** + * Event source for `sendCustom` events + * @version Added in JS SDK 0.1 + */ + custom: Contract; + /** + * Event source for navigation events (back key presses) + * @version Added in JS SDK 0.1 + */ + navigation: Contract; + /** + * Sends a number to the custom event handler + * @param event number to send + * @version Added in JS SDK 0.1 + */ + sendCustom(event: number): void; + /** + * Switches to a view + * @param assoc View-ViewDispatcher association as returned by `add` + * @version Added in JS SDK 0.1 + */ + switchTo(assoc: View): void; + /** + * Sends this ViewDispatcher to the front or back, above or below all other + * GUI viewports + * @param direction Either `"front"` or `"back"` + * @version Added in JS SDK 0.1 + */ + sendTo(direction: "front" | "back"): void; +} + +/** + * @version Added in JS SDK 0.1 + */ +export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts new file mode 100644 index 000000000..b8b10c43a --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts @@ -0,0 +1,33 @@ +/** + * Displays an animated hourglass icon. Suppresses all `navigation` events, + * making it impossible for the user to exit the view by pressing the back key. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let loadingView = require("gui/loading"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * This view does not have any props. + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class Loading extends View { } +declare class LoadingFactory extends ViewFactory { } +declare const factory: LoadingFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts new file mode 100644 index 000000000..31e08aab8 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts @@ -0,0 +1,39 @@ +/** + * Displays a scrollable list of clickable textual entries. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let submenuView = require("gui/submenu"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * - `header`: Text displayed at the top of the screen in bold + * - `items`: Array of selectable textual items + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + items: string[], +}; +declare class Submenu extends View { + chosen: Contract; +} +declare class SubmenuFactory extends ViewFactory { } +declare const factory: SubmenuFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts new file mode 100644 index 000000000..a46ec73fa --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts @@ -0,0 +1,41 @@ +/** + * Displays a scrollable read-only text field. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let textBoxView = require("gui/text_box"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `text`: Text in the text box + * - `font`: The font to display the text in (`"text"` or `"hex"`) + * - `focus`: The initial focus of the text box (`"start"` or `"end"`) + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + text: string, + font: "text" | "hex", + focus: "start" | "end", +} +declare class TextBox extends View { + chosen: Contract; +} +declare class TextBoxFactory extends ViewFactory { } +declare const factory: TextBoxFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts new file mode 100644 index 000000000..2c890df57 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts @@ -0,0 +1,41 @@ +/** + * Displays a keyboard. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let textInputView = require("gui/text_input"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the `gui.js` example script. + * + * # View props + * - `header`: Text displayed at the top of the screen + * - `minLength`: Minimum allowed text length + * - `maxLength`: Maximum allowed text length + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + minLength: number, + maxLength: number, +} +declare class TextInput extends View { + input: Contract; +} +declare class TextInputFactory extends ViewFactory { } +declare const factory: TextInputFactory; +export = factory; diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/packages/fz-sdk/math/index.d.ts similarity index 51% rename from applications/system/js_app/types/math/index.d.ts rename to applications/system/js_app/packages/fz-sdk/math/index.d.ts index 25abca4af..ff84cdcb0 100644 --- a/applications/system/js_app/types/math/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/math/index.d.ts @@ -1,24 +1,54 @@ +/** + * Math operations + * @version Added in JS SDK 0.1 + * @module + */ + +/** @version Added in JS SDK 0.1 */ export function abs(n: number): number; +/** @version Added in JS SDK 0.1 */ export function acos(n: number): number; +/** @version Added in JS SDK 0.1 */ export function acosh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function asin(n: number): number; +/** @version Added in JS SDK 0.1 */ export function asinh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function atan(n: number): number; +/** @version Added in JS SDK 0.1 */ export function atan2(a: number, b: number): number; +/** @version Added in JS SDK 0.1 */ export function atanh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function cbrt(n: number): number; +/** @version Added in JS SDK 0.1 */ export function ceil(n: number): number; +/** @version Added in JS SDK 0.1 */ export function clz32(n: number): number; +/** @version Added in JS SDK 0.1 */ export function cos(n: number): number; +/** @version Added in JS SDK 0.1 */ export function exp(n: number): number; +/** @version Added in JS SDK 0.1 */ export function floor(n: number): number; +/** @version Added in JS SDK 0.1 */ export function max(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function min(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function pow(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function random(): number; +/** @version Added in JS SDK 0.1 */ export function sign(n: number): number; +/** @version Added in JS SDK 0.1 */ export function sin(n: number): number; +/** @version Added in JS SDK 0.1 */ export function sqrt(n: number): number; +/** @version Added in JS SDK 0.1 */ export function trunc(n: number): number; +/** @version Added in JS SDK 0.1 */ declare const PI: number; +/** @version Added in JS SDK 0.1 */ declare const EPSILON: number; diff --git a/applications/system/js_app/types/notification/index.d.ts b/applications/system/js_app/packages/fz-sdk/notification/index.d.ts similarity index 75% rename from applications/system/js_app/types/notification/index.d.ts rename to applications/system/js_app/packages/fz-sdk/notification/index.d.ts index 947daba21..2199a1479 100644 --- a/applications/system/js_app/types/notification/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/notification/index.d.ts @@ -1,6 +1,13 @@ +/** + * Module for using the color LED and vibration motor + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Signals success to the user via the color LED, speaker and vibration * motor + * @version Added in JS SDK 0.1 */ export declare function success(): void; @@ -10,11 +17,15 @@ export declare function success(): void; */ export declare function error(): void; +/** + * @version Added in JS SDK 0.1 + */ export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta"; /** * @brief Displays a basic color on the color LED * @param color The color to display, see `Color` * @param duration The duration, either `"short"` (10ms) or `"long"` (100ms) + * @version Added in JS SDK 0.1 */ export declare function blink(color: Color, duration: "short" | "long"): void; diff --git a/applications/system/js_app/packages/fz-sdk/package.json b/applications/system/js_app/packages/fz-sdk/package.json new file mode 100644 index 000000000..219b2645b --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/package.json @@ -0,0 +1,29 @@ +{ + "name": "@flipperdevices/fz-sdk", + "version": "0.1.0", + "description": "Type declarations and documentation for native JS modules available on Flipper Zero", + "keywords": [ + "flipper", + "flipper zero", + "framework" + ], + "author": "Flipper Devices", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git+https://github.com/flipperdevices/flipperzero-firmware.git", + "directory": "applications/system/js_app/packages/fz-sdk" + }, + "type": "module", + "devDependencies": { + "esbuild": "^0.24.0", + "esbuild-plugin-tsc": "^0.4.0", + "json5": "^2.2.3", + "typedoc": "^0.26.10", + "typedoc-material-theme": "^1.1.0" + }, + "dependencies": { + "prompts": "^2.4.2", + "serialport": "^12.0.0" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml b/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml new file mode 100644 index 000000000..45944a854 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml @@ -0,0 +1,896 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + esbuild: + specifier: ^0.24.0 + version: 0.24.0 + esbuild-plugin-tsc: + specifier: ^0.4.0 + version: 0.4.0(typescript@5.6.3) + json5: + specifier: ^2.2.3 + version: 2.2.3 + typedoc: + specifier: ^0.26.10 + version: 0.26.10(typescript@5.6.3) + typedoc-material-theme: + specifier: ^1.1.0 + version: 1.1.0(typedoc@0.26.10(typescript@5.6.3)) + +packages: + + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@material/material-color-utilities@0.2.7': + resolution: {integrity: sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==} + + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@shikijs/core@1.22.0': + resolution: {integrity: sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==} + + '@shikijs/engine-javascript@1.22.0': + resolution: {integrity: sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==} + + '@shikijs/engine-oniguruma@1.22.0': + resolution: {integrity: sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==} + + '@shikijs/types@1.22.0': + resolution: {integrity: sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==} + + '@shikijs/vscode-textmate@9.3.0': + resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild-plugin-tsc@0.4.0: + resolution: {integrity: sha512-q9gWIovt1nkwchMLc2zhyksaiHOv3kDK4b0AUol8lkMCRhJ1zavgfb2fad6BKp7FT9rh/OHmEBXVjczLoi/0yw==} + peerDependencies: + typescript: ^4.0.0 || ^5.0.0 + + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + + hast-util-to-html@9.0.3: + resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + oniguruma-to-js@0.4.3: + resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + regex@4.3.3: + resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==} + + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + shiki@1.22.0: + resolution: {integrity: sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + typedoc-material-theme@1.1.0: + resolution: {integrity: sha512-LLWGVb8w+i+QGnsu/a0JKjcuzndFQt/UeGVOQz0HFFGGocROEHv5QYudIACrj+phL2LDwH05tJx0Ob3pYYH2UA==} + engines: {node: '>=18.0.0', npm: '>=8.6.0'} + peerDependencies: + typedoc: ^0.25.13 || ^0.26.3 + + typedoc@0.26.10: + resolution: {integrity: sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==} + engines: {node: '>= 18'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@esbuild/aix-ppc64@0.24.0': + optional: true + + '@esbuild/android-arm64@0.24.0': + optional: true + + '@esbuild/android-arm@0.24.0': + optional: true + + '@esbuild/android-x64@0.24.0': + optional: true + + '@esbuild/darwin-arm64@0.24.0': + optional: true + + '@esbuild/darwin-x64@0.24.0': + optional: true + + '@esbuild/freebsd-arm64@0.24.0': + optional: true + + '@esbuild/freebsd-x64@0.24.0': + optional: true + + '@esbuild/linux-arm64@0.24.0': + optional: true + + '@esbuild/linux-arm@0.24.0': + optional: true + + '@esbuild/linux-ia32@0.24.0': + optional: true + + '@esbuild/linux-loong64@0.24.0': + optional: true + + '@esbuild/linux-mips64el@0.24.0': + optional: true + + '@esbuild/linux-ppc64@0.24.0': + optional: true + + '@esbuild/linux-riscv64@0.24.0': + optional: true + + '@esbuild/linux-s390x@0.24.0': + optional: true + + '@esbuild/linux-x64@0.24.0': + optional: true + + '@esbuild/netbsd-x64@0.24.0': + optional: true + + '@esbuild/openbsd-arm64@0.24.0': + optional: true + + '@esbuild/openbsd-x64@0.24.0': + optional: true + + '@esbuild/sunos-x64@0.24.0': + optional: true + + '@esbuild/win32-arm64@0.24.0': + optional: true + + '@esbuild/win32-ia32@0.24.0': + optional: true + + '@esbuild/win32-x64@0.24.0': + optional: true + + '@material/material-color-utilities@0.2.7': {} + + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@shikijs/core@1.22.0': + dependencies: + '@shikijs/engine-javascript': 1.22.0 + '@shikijs/engine-oniguruma': 1.22.0 + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + + '@shikijs/engine-javascript@1.22.0': + dependencies: + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + oniguruma-to-js: 0.4.3 + + '@shikijs/engine-oniguruma@1.22.0': + dependencies: + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/types@1.22.0': + dependencies: + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@9.3.0': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.2.0': {} + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@4.5.0: {} + + esbuild-plugin-tsc@0.4.0(typescript@5.6.3): + dependencies: + strip-comments: 2.0.1 + typescript: 5.6.3 + + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + + hast-util-to-html@9.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-void-elements@3.0.0: {} + + json5@2.2.3: {} + + kleur@3.0.3: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lunr@2.3.9: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + ms@2.1.2: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + oniguruma-to-js@0.4.3: + dependencies: + regex: 4.3.3 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + punycode.js@2.3.1: {} + + regex@4.3.3: {} + + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + shiki@1.22.0: + dependencies: + '@shikijs/core': 1.22.0 + '@shikijs/engine-javascript': 1.22.0 + '@shikijs/engine-oniguruma': 1.22.0 + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + sisteransi@1.0.5: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-comments@2.0.1: {} + + trim-lines@3.0.1: {} + + typedoc-material-theme@1.1.0(typedoc@0.26.10(typescript@5.6.3)): + dependencies: + '@material/material-color-utilities': 0.2.7 + typedoc: 0.26.10(typescript@5.6.3) + + typedoc@0.26.10(typescript@5.6.3): + dependencies: + lunr: 2.3.9 + markdown-it: 14.1.0 + minimatch: 9.0.5 + shiki: 1.22.0 + typescript: 5.6.3 + yaml: 2.6.0 + + typescript@5.6.3: {} + + uc.micro@2.1.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + yaml@2.6.0: {} + + zwitch@2.0.4: {} diff --git a/applications/system/js_app/packages/fz-sdk/sdk.js b/applications/system/js_app/packages/fz-sdk/sdk.js new file mode 100644 index 000000000..2eecf032d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/sdk.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { SerialPort } from "serialport"; +import prompts from "prompts"; +import esbuild from "esbuild"; +import json5 from "json5"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function build(config) { + await esbuild.build({ + entryPoints: ["./dist/index.js"], + outfile: config.output, + tsconfig: "./tsconfig.json", + format: "cjs", + bundle: true, + minify: config.minify, + external: [ + "@flipperdevices/fz-sdk/*" + ], + supported: { + "array-spread": false, + "arrow": false, + "async-await": false, + "async-generator": false, + "bigint": false, + "class": false, + "const-and-let": true, + "decorators": false, + "default-argument": false, + "destructuring": false, + "dynamic-import": false, + "exponent-operator": false, + "export-star-as": false, + "for-await": false, + "for-of": false, + "function-name-configurable": false, + "function-or-class-property-access": false, + "generator": false, + "hashbang": false, + "import-assertions": false, + "import-meta": false, + "inline-script": false, + "logical-assignment": false, + "nested-rest-binding": false, + "new-target": false, + "node-colon-prefix-import": false, + "node-colon-prefix-require": false, + "nullish-coalescing": false, + "object-accessors": false, + "object-extensions": false, + "object-rest-spread": false, + "optional-catch-binding": false, + "optional-chain": false, + "regexp-dot-all-flag": false, + "regexp-lookbehind-assertions": false, + "regexp-match-indices": false, + "regexp-named-capture-groups": false, + "regexp-set-notation": false, + "regexp-sticky-and-unicode-flags": false, + "regexp-unicode-property-escapes": false, + "rest-argument": false, + "template-literal": false, + "top-level-await": false, + "typeof-exotic-object-is-object": false, + "unicode-escapes": false, + "using": false, + }, + }); + + let outContents = fs.readFileSync(config.output, "utf8"); + outContents = "let exports = {};\n" + outContents; + + if (config.enforceSdkVersion) { + const version = json5.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version; + let [major, minor, _] = version.split("."); + outContents = `checkSdkCompatibility(${major}, ${minor});\n${outContents}`; + } + + fs.writeFileSync(config.output, outContents); +} + +async function upload(config) { + const appFile = fs.readFileSync(config.input, "utf8"); + const flippers = (await SerialPort.list()).filter(x => x.serialNumber?.startsWith("flip_")); + + if (!flippers) { + console.error("No Flippers found"); + process.exit(1); + } + + let portPath = flippers[0].path; + if (flippers.length > 1) { + port = (await prompts([{ + type: "select", + name: "port", + message: "Select Flipper to run the app on", + choices: flippers.map(x => ({ title: x.serialNumber.replace("flip_", ""), value: x.path })), + }])).port; + } + + console.log(`Connecting to Flipper at ${portPath}`); + let port = new SerialPort({ path: portPath, baudRate: 230400 }); + let received = ""; + let lastMatch = 0; + async function waitFor(string, timeoutMs) { + return new Promise((resolve, _reject) => { + let timeout = undefined; + if (timeoutMs) { + timeout = setTimeout(() => { + console.error("Error: timeout"); + process.exit(1); + }, timeoutMs); + } + setInterval(() => { + let idx = received.indexOf(string, lastMatch); + if (idx !== -1) { + lastMatch = idx; + if (timeoutMs) + clearTimeout(timeout); + resolve(); + } + }, 50); + }); + } + port.on("data", (data) => { + received += data.toString(); + }); + + await waitFor(">: ", 1000); + console.log("Uploading application file"); + port.write(`storage remove ${config.output}\x0d`); + port.drain(); + await waitFor(">: ", 1000); + port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`); + await waitFor("Ready", 1000); + port.write(appFile); + port.drain(); + await waitFor(">: ", 1000); + + console.log("Launching application"); + port.write(`js ${config.output}\x0d`); + port.drain(); + + await waitFor("Running", 1000); + process.stdout.write(received.slice(lastMatch)); + port.on("data", (data) => { + process.stdout.write(data.toString()); + }); + process.on("exit", () => { + port.write("\x03"); + }); + + await waitFor("Script done!", 0); + process.exit(0); +} + +(async () => { + const commands = { + "build": build, + "upload": upload, + }; + + const config = json5.parse(fs.readFileSync("./fz-sdk.config.json5", "utf8")); + const command = process.argv[2]; + + if (!Object.keys(commands).includes(command)) { + console.error(`Unknown command ${command}. Supported: ${Object.keys(commands).join(", ")}`); + process.exit(1); + } + + await commands[command](config[command]); +})(); diff --git a/applications/system/js_app/types/serial/index.d.ts b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts similarity index 93% rename from applications/system/js_app/types/serial/index.d.ts rename to applications/system/js_app/packages/fz-sdk/serial/index.d.ts index 1a7ed6397..9a28bd5e3 100644 --- a/applications/system/js_app/types/serial/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts @@ -1,7 +1,14 @@ +/** + * Module for accessing the serial port + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Initializes the serial port * @param port The port to initialize (`"lpuart"` or `"start"`) * @param baudRate + * @version Added in JS SDK 0.1 */ export declare function setup(port: "lpuart" | "usart", baudRate: number): void; @@ -13,6 +20,7 @@ export declare function setup(port: "lpuart" | "usart", baudRate: number): void; * - Arrays of numbers will get sent as a sequence of bytes. * - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence * of bytes. + * @version Added in JS SDK 0.1 */ export declare function write(value: string | number | number[] | ArrayBuffer | TypedArray): void; @@ -24,6 +32,7 @@ export declare function write(value: string | number | nu * unset, the function will wait forever. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function read(length: number, timeout?: number): string | undefined; @@ -39,6 +48,7 @@ export declare function read(length: number, timeout?: number): string | undefin * applies to characters, not entire strings. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function readln(timeout?: number): string; @@ -50,6 +60,7 @@ export declare function readln(timeout?: number): string; * unset, the function will wait forever. * @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were * read. + * @version Added in JS SDK 0.1 */ export declare function readBytes(length: number, timeout?: number): ArrayBuffer; @@ -73,5 +84,6 @@ export declare function readBytes(length: number, timeout?: number): ArrayBuffer * @returns The index of the matched pattern if multiple were provided, or 0 if * only one was provided and it matched, or `undefined` if none of the * patterns matched. + * @version Added in JS SDK 0.1 */ export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined; diff --git a/applications/system/js_app/types/storage/index.d.ts b/applications/system/js_app/packages/fz-sdk/storage/index.d.ts similarity index 83% rename from applications/system/js_app/types/storage/index.d.ts rename to applications/system/js_app/packages/fz-sdk/storage/index.d.ts index 0dd29e121..90d7a8254 100644 --- a/applications/system/js_app/types/storage/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/storage/index.d.ts @@ -1,8 +1,15 @@ +/** + * Module for accessing the filesystem + * @version Added in JS SDK 0.1 + * @module + */ + /** * File readability mode: * - `"r"`: read-only * - `"w"`: write-only * - `"rw"`: read-write + * @version Added in JS SDK 0.1 */ export type AccessMode = "r" | "w" | "rw"; @@ -13,53 +20,78 @@ export type AccessMode = "r" | "w" | "rw"; * - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist * - `"create_new"`: create new file or fail if it exists * - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist + * @version Added in JS SDK 0.1 */ export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always"; -/** Standard UNIX timestamp */ +/** + * Standard UNIX timestamp + * @version Added in JS SDK 0.1 + */ export type Timestamp = number; -/** File information structure */ +/** + * File information structure + * @version Added in JS SDK 0.1 + */ export declare class FileInfo { /** * Full path (e.g. "/ext/test", returned by `stat`) or file name * (e.g. "test", returned by `readDirectory`) + * @version Added in JS SDK 0.1 */ path: string; /** * Is the file a directory? + * @version Added in JS SDK 0.1 */ isDirectory: boolean; /** * File size in bytes, or 0 in the case of directories + * @version Added in JS SDK 0.1 */ size: number; /** * Time of last access as a UNIX timestamp + * @version Added in JS SDK 0.1 */ accessTime: Timestamp; } -/** Filesystem information structure */ +/** + * Filesystem information structure + * @version Added in JS SDK 0.1 + */ export declare class FsInfo { - /** Total size of the filesystem, in bytes */ + /** + * Total size of the filesystem, in bytes + * @version Added in JS SDK 0.1 + */ totalSpace: number; - /** Free space in the filesystem, in bytes */ + /** + * Free space in the filesystem, in bytes + * @version Added in JS SDK 0.1 + */ freeSpace: number; } // file operations -/** File class */ +/** + * File class + * @version Added in JS SDK 0.1 + */ export declare class File { /** * Closes the file. After this method is called, all other operations * related to this file become unavailable. * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ close(): boolean; /** * Is the file currently open? + * @version Added in JS SDK 0.1 */ isOpen(): boolean; /** @@ -70,6 +102,7 @@ export declare class File { * @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode * is `ascii`. The number of bytes that was actually read may be * fewer than requested. + * @version Added in JS SDK 0.1 */ read(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T; /** @@ -77,36 +110,43 @@ export declare class File { * @param data The data to write: a string that will be ASCII-encoded, or an * ArrayBuf * @returns the amount of bytes that was actually written + * @version Added in JS SDK 0.1 */ write(data: ArrayBuffer | string): number; /** * Moves the R/W pointer forward * @param bytes How many bytes to move the pointer forward by * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ seekRelative(bytes: number): boolean; /** * Moves the R/W pointer to an absolute position inside the file * @param bytes The position inside the file * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ seekAbsolute(bytes: number): boolean; /** * Gets the absolute position of the R/W pointer in bytes + * @version Added in JS SDK 0.1 */ tell(): number; /** * Discards the data after the current position of the R/W pointer in a file * opened in either write-only or read-write mode. * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ truncate(): boolean; /** * Reads the total size of the file in bytes + * @version Added in JS SDK 0.1 */ size(): number; /** * Detects whether the R/W pointer has reached the end of the file + * @version Added in JS SDK 0.1 */ eof(): boolean; /** @@ -115,6 +155,7 @@ export declare class File { * @param dest The file to copy the bytes into * @param bytes The number of bytes to copy * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ copyTo(dest: File, bytes: number): boolean; } @@ -126,12 +167,14 @@ export declare class File { * @param openMode `"open_existing"`, `"open_always"`, `"open_append"`, * `"create_new"` or `"create_always"`; see `OpenMode` * @returns a `File` on success, or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined; /** * Detects whether a file exists * @param path The path to the file * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function fileExists(path: string): boolean; @@ -142,17 +185,20 @@ export declare function fileExists(path: string): boolean; * @param path The path to the directory * @returns Array of `FileInfo` structures with directory entries, * or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function readDirectory(path: string): FileInfo[] | undefined; /** * Detects whether a directory exists * @param path The path to the directory + * @version Added in JS SDK 0.1 */ export declare function directoryExists(path: string): boolean; /** * Creates an empty directory * @param path The path to the new directory * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function makeDirectory(path: string): boolean; @@ -161,24 +207,28 @@ export declare function makeDirectory(path: string): boolean; /** * Detects whether a file or a directory exists * @param path The path to the file or directory + * @version Added in JS SDK 0.1 */ export declare function fileOrDirExists(path: string): boolean; /** * Acquires metadata about a file or directory * @param path The path to the file or directory * @returns A `FileInfo` structure or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function stat(path: string): FileInfo | undefined; /** * Removes a file or an empty directory * @param path The path to the file or directory * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function remove(path: string): boolean; /** * Removes a file or recursively removes a possibly non-empty directory * @param path The path to the file or directory * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function rmrf(path: string): boolean; /** @@ -187,6 +237,7 @@ export declare function rmrf(path: string): boolean; * @param newPath The new path that the file or directory will become accessible * under * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function rename(oldPath: string, newPath: string): boolean; /** @@ -194,11 +245,13 @@ export declare function rename(oldPath: string, newPath: string): boolean; * @param oldPath The original path to the file or directory * @param newPath The new path that the copy of the file or directory will be * accessible under + * @version Added in JS SDK 0.1 */ export declare function copy(oldPath: string, newPath: string): boolean; /** * Fetches generic information about a filesystem * @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`) + * @version Added in JS SDK 0.1 */ export declare function fsInfo(filesystem: string): FsInfo | undefined; /** @@ -218,6 +271,7 @@ export declare function fsInfo(filesystem: string): FsInfo | undefined; * @param maxLen The maximum length of the filename with the numeric suffix * @returns The base of the filename with the next available numeric suffix, * without the extension or the base directory. + * @version Added in JS SDK 0.1 */ export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string; @@ -226,6 +280,7 @@ export declare function nextAvailableFilename(dirPath: string, fileName: string, /** * Determines whether the two paths are equivalent. Respects filesystem-defined * path equivalence rules. + * @version Added in JS SDK 0.1 */ export declare function arePathsEqual(path1: string, path2: string): boolean; /** @@ -233,5 +288,6 @@ export declare function arePathsEqual(path1: string, path2: string): boolean; * filesystem-defined path equivalence rules. * @param parentPath The parent path * @param childPath The child path + * @version Added in JS SDK 0.1 */ export declare function isSubpathOf(parentPath: string, childPath: string): boolean; diff --git a/applications/system/js_app/types/tests/index.d.ts b/applications/system/js_app/packages/fz-sdk/tests/index.d.ts similarity index 88% rename from applications/system/js_app/types/tests/index.d.ts rename to applications/system/js_app/packages/fz-sdk/tests/index.d.ts index 8aaeec5e5..031588d4a 100644 --- a/applications/system/js_app/types/tests/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/tests/index.d.ts @@ -1,6 +1,8 @@ /** * Unit test module. Only available if the firmware has been configured with * `FIRMWARE_APP_SET=unit_tests`. + * @version Added in JS SDK 0.1 + * @module */ export function fail(message: string): never; diff --git a/applications/system/js_app/packages/fz-sdk/tsconfig.json b/applications/system/js_app/packages/fz-sdk/tsconfig.json new file mode 100644 index 000000000..cfb792e3f --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "CommonJS", + "noLib": true, + }, + "include": [ + "./**/*.d.ts" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/typedoc.json b/applications/system/js_app/packages/fz-sdk/typedoc.json new file mode 100644 index 000000000..8b3befa6d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/typedoc.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "name": "Flipper Zero JS API", + "excludePrivate": true, + "entryPointStrategy": "expand", + "entryPoints": [ + ".", + ], + "exclude": [ + "node_modules" + ], + "cleanOutputDir": true, + "out": "./docs", + "plugin": [ + "typedoc-material-theme", + ], + "readme": "./docs_readme.md", + "themeColor": "#ff8200", +} \ No newline at end of file diff --git a/applications/system/js_app/types/event_loop/index.d.ts b/applications/system/js_app/types/event_loop/index.d.ts deleted file mode 100644 index 49237782c..000000000 --- a/applications/system/js_app/types/event_loop/index.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -type Lit = undefined | null | {}; - -/** - * Subscription control interface - */ -export interface Subscription { - /** - * Cancels the subscription, preventing any future events managed by the - * subscription from firing - */ - cancel(): void; -} - -/** - * Opaque event source identifier - */ -export type Contract = symbol; - -/** - * A callback can be assigned to an event loop to listen to an event. It may - * return an array with values that will be passed to it as arguments the next - * time that it is called. The first argument is always the subscription - * manager, and the second argument is always the item that trigged the event. - * The type of the item is defined by the event source. - */ -export type Callback = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void; - -/** - * Subscribes a callback to an event - * @param contract Event identifier - * @param callback Function to call when the event is triggered - * @param args Initial arguments passed to the callback - */ -export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; -/** - * Runs the event loop until it is stopped (potentially never) - */ -export function run(): void | never; -/** - * Stops the event loop - */ -export function stop(): void; - -/** - * Creates a timer event that can be subscribed to just like any other event - * @param mode Either `"oneshot"` or `"periodic"` - * @param interval Timer interval in milliseconds - */ -export function timer(mode: "oneshot" | "periodic", interval: number): Contract; - -/** - * Message queue - */ -export interface Queue { - /** - * Message event - */ - input: Contract; - /** - * Sends a message to the queue - * @param message message to send - */ - send(message: T): void; -} - -/** - * Creates a message queue - * @param length maximum queue capacity - */ -export function queue(length: number): Queue; diff --git a/applications/system/js_app/types/flipper/index.d.ts b/applications/system/js_app/types/flipper/index.d.ts deleted file mode 100644 index b1b1d474b..000000000 --- a/applications/system/js_app/types/flipper/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @brief Returns the device model - */ -export declare function getModel(): string; - -/** - * @brief Returns the name of the virtual dolphin - */ -export declare function getName(): string; - -/** - * @brief Returns the battery charge percentage - */ -export declare function getBatteryCharge(): number; diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts deleted file mode 100644 index ab1660cf6..000000000 --- a/applications/system/js_app/types/global.d.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @brief Pauses JavaScript execution for a while - * @param ms How many milliseconds to pause the execution for - */ -declare function delay(ms: number): void; - -/** - * @brief Prints to the GUI console view - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the console view - */ -declare function print(...args: any[]): void; - -/** - * @brief Converts a number to a string - * @param value The number to convert to a string - * @param base Integer base (`2`...`16`), default: 16 - */ -declare function toString(value: number, base?: number): string; - -/** - * @brief Reads a JS value from a file - * - * Reads a file at the specified path, interprets it as a JS value and returns - * said value. - * - * @param path The path to the file - */ -declare function load(path: string): any; - -/** - * @brief mJS Foreign Pointer type - * - * JavaScript code cannot do anything with values of `RawPointer` type except - * acquire them from native code and pass them right back to other parts of - * native code. These values cannot be turned into something meaningful, nor can - * be they modified. - */ -declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; -// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. - -/** - * @brief Holds raw bytes - */ -declare class ArrayBuffer { - /** - * @brief The pointer to the byte buffer - * @note Like other `RawPointer` values, this value is essentially useless - * to JS code. - */ - getPtr: RawPointer; - /** - * @brief The length of the buffer in bytes - */ - byteLength: number; - /** - * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer - * @param start The index of the byte in the source buffer to be used as the - * start for the new buffer - * @param end The index of the byte in the source buffer that follows the - * byte to be used as the last byte for the new buffer - */ - slice(start: number, end?: number): ArrayBuffer; -} - -declare function ArrayBuffer(): ArrayBuffer; - -declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; - -declare class TypedArray { - /** - * @brief The length of the buffer in bytes - */ - byteLength: number; - /** - * @brief The length of the buffer in typed elements - */ - length: number; - /** - * @brief The underlying `ArrayBuffer` - */ - buffer: ArrayBuffer; -} - -declare class Uint8Array extends TypedArray<"u8"> { } -declare class Int8Array extends TypedArray<"i8"> { } -declare class Uint16Array extends TypedArray<"u16"> { } -declare class Int16Array extends TypedArray<"i16"> { } -declare class Uint32Array extends TypedArray<"u32"> { } -declare class Int32Array extends TypedArray<"i32"> { } - -declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; -declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; -declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; -declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; -declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; -declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; - -declare const console: { - /** - * @brief Prints to the UART logs at the `[I]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - log(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[D]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - debug(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[W]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - warn(...args: any[]): void; - /** - * @brief Prints to the UART logs at the `[E]` level - * @param args The arguments are converted to strings, concatenated without any - * spaces in between and printed to the logs - */ - error(...args: any[]): void; -}; - -declare class Array { - /** - * @brief Takes items out of the array - * - * Removes elements from the array and returns them in a new array - * - * @param start The index to start taking elements from - * @param deleteCount How many elements to take - * @returns The elements that were taken out of the original array as a new - * array - */ - splice(start: number, deleteCount: number): T[]; - /** - * @brief Adds a value to the end of the array - * @param value The value to add - * @returns New length of the array - */ - push(value: T): number; - /** - * @brief How many elements there are in the array - */ - length: number; -} - -declare class String { - /** - * @brief How many characters there are in the string - */ - length: number; - /** - * @brief Returns the character code at an index in the string - * @param index The index to consult - */ - charCodeAt(index: number): number; - /** - * See `charCodeAt` - */ - at(index: number): number; -} - -declare class Boolean { } - -declare class Function { } - -declare class Number { } - -declare class Object { } - -declare class RegExp { } - -declare interface IArguments { } - -declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gui/dialog.d.ts b/applications/system/js_app/types/gui/dialog.d.ts deleted file mode 100644 index 6d9c8d43b..000000000 --- a/applications/system/js_app/types/gui/dialog.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - text: string, - left: string, - center: string, - right: string, -} -declare class Dialog extends View { - input: Contract<"left" | "center" | "right">; -} -declare class DialogFactory extends ViewFactory { } -declare const factory: DialogFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/empty_screen.d.ts b/applications/system/js_app/types/gui/empty_screen.d.ts deleted file mode 100644 index c71e93b32..000000000 --- a/applications/system/js_app/types/gui/empty_screen.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { View, ViewFactory } from "."; - -type Props = {}; -declare class EmptyScreen extends View { } -declare class EmptyScreenFactory extends ViewFactory { } -declare const factory: EmptyScreenFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts deleted file mode 100644 index 3f95ab780..000000000 --- a/applications/system/js_app/types/gui/index.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Contract } from "../event_loop"; - -type Properties = { [K: string]: any }; - -export declare class View { - set

(property: P, value: Props[P]): void; -} - -export declare class ViewFactory> { - make(): V; - makeWith(initial: Partial): V; -} - -declare class ViewDispatcher { - /** - * Event source for `sendCustom` events - */ - custom: Contract; - /** - * Event source for navigation events (back key presses) - */ - navigation: Contract; - /** - * Sends a number to the custom event handler - * @param event number to send - */ - sendCustom(event: number): void; - /** - * Switches to a view - * @param assoc View-ViewDispatcher association as returned by `add` - */ - switchTo(assoc: View): void; - /** - * Sends this ViewDispatcher to the front or back, above or below all other - * GUI viewports - * @param direction Either `"front"` or `"back"` - */ - sendTo(direction: "front" | "back"): void; -} - -export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/types/gui/loading.d.ts b/applications/system/js_app/types/gui/loading.d.ts deleted file mode 100644 index 73a963349..000000000 --- a/applications/system/js_app/types/gui/loading.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { View, ViewFactory } from "."; - -type Props = {}; -declare class Loading extends View { } -declare class LoadingFactory extends ViewFactory { } -declare const factory: LoadingFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/submenu.d.ts b/applications/system/js_app/types/gui/submenu.d.ts deleted file mode 100644 index 59d535864..000000000 --- a/applications/system/js_app/types/gui/submenu.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - items: string[], -}; -declare class Submenu extends View { - chosen: Contract; -} -declare class SubmenuFactory extends ViewFactory { } -declare const factory: SubmenuFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/text_box.d.ts b/applications/system/js_app/types/gui/text_box.d.ts deleted file mode 100644 index 3dbbac571..000000000 --- a/applications/system/js_app/types/gui/text_box.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - text: string, - font: "text" | "hex", - focus: "start" | "end", -} -declare class TextBox extends View { - chosen: Contract; -} -declare class TextBoxFactory extends ViewFactory { } -declare const factory: TextBoxFactory; -export = factory; diff --git a/applications/system/js_app/types/gui/text_input.d.ts b/applications/system/js_app/types/gui/text_input.d.ts deleted file mode 100644 index 96652b1d4..000000000 --- a/applications/system/js_app/types/gui/text_input.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - minLength: number, - maxLength: number, -} -declare class TextInput extends View { - input: Contract; -} -declare class TextInputFactory extends ViewFactory { } -declare const factory: TextInputFactory; -export = factory; diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md index 9791fb4eb..aa444bacd 100644 --- a/documentation/js/js_gpio.md +++ b/documentation/js/js_gpio.md @@ -61,7 +61,7 @@ Reads a digital value from a pin configured with `direction: "in"` and any #### Returns Boolean logic level -### `Pin.read_analog()` +### `Pin.readAnalog()` Reads an analog voltage level in millivolts from a pin configured with `direction: "in"` and `inMode: "analog"` diff --git a/lib/mjs/mjs_builtin.c b/lib/mjs/mjs_builtin.c index afcf9ce6f..7a1d74ff1 100644 --- a/lib/mjs/mjs_builtin.c +++ b/lib/mjs/mjs_builtin.c @@ -146,10 +146,17 @@ void mjs_init_builtin(struct mjs* mjs, mjs_val_t obj) { // mjs_set(mjs, obj, "JSON", ~0, v); /* - * Populate Object.create() + * Populate Object */ v = mjs_mk_object(mjs); mjs_set(mjs, v, "create", ~0, mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_op_create_object)); + mjs_set( + mjs, + v, + "defineProperty", + ~0, + mjs_mk_foreign_func( + mjs, (mjs_func_ptr_t)mjs_op_object_define_property)); // stub, do not use mjs_set(mjs, obj, "Object", ~0, v); /* diff --git a/lib/mjs/mjs_object.c b/lib/mjs/mjs_object.c index 60bacf514..cf14a499f 100644 --- a/lib/mjs/mjs_object.c +++ b/lib/mjs/mjs_object.c @@ -294,6 +294,11 @@ clean: mjs_return(mjs, ret); } +MJS_PRIVATE void mjs_op_object_define_property(struct mjs* mjs) { + // stub, do not use + mjs_return(mjs, MJS_UNDEFINED); +} + mjs_val_t mjs_struct_to_obj(struct mjs* mjs, const void* base, const struct mjs_c_struct_member* defs) { mjs_val_t obj; diff --git a/lib/mjs/mjs_object.h b/lib/mjs/mjs_object.h index 870486d06..101272e29 100644 --- a/lib/mjs/mjs_object.h +++ b/lib/mjs/mjs_object.h @@ -50,6 +50,11 @@ MJS_PRIVATE mjs_err_t mjs_set_internal( */ MJS_PRIVATE void mjs_op_create_object(struct mjs* mjs); +/* + * Stub of `Object.defineProperty()` + */ +MJS_PRIVATE void mjs_op_object_define_property(struct mjs* mjs); + /* * Cell destructor for object arena */ diff --git a/lib/mjs/mjs_parser.c b/lib/mjs/mjs_parser.c index 503b16942..212804a86 100644 --- a/lib/mjs/mjs_parser.c +++ b/lib/mjs/mjs_parser.c @@ -76,7 +76,8 @@ static int s_assign_ops[] = { static int findtok(int* toks, int tok) { int i = 0; - while(tok != toks[i] && toks[i] != TOK_EOF) i++; + while(tok != toks[i] && toks[i] != TOK_EOF) + i++; return toks[i]; } @@ -87,7 +88,7 @@ static void emit_op(struct pstate* pstate, int tok) { } #define BINOP_STACK_FRAME_SIZE 16 -#define STACK_LIMIT 8192 +#define STACK_LIMIT 8192 // Intentionally left as macro rather than a function, to let the // compiler to inline calls and mimimize runtime stack usage. @@ -166,7 +167,8 @@ static mjs_err_t parse_statement_list(struct pstate* p, int et) { if(drop) emit_byte(p, OP_DROP); res = parse_statement(p); drop = 1; - while(p->tok.tok == TOK_SEMICOLON) pnext1(p); + while(p->tok.tok == TOK_SEMICOLON) + pnext1(p); } /* @@ -523,7 +525,11 @@ static mjs_err_t parse_expr(struct pstate* p) { static mjs_err_t parse_let(struct pstate* p) { mjs_err_t res = MJS_OK; LOG(LL_VERBOSE_DEBUG, ("[%.*s]", 10, p->tok.ptr)); - EXPECT(p, TOK_KEYWORD_LET); + if((p)->tok.tok != TOK_KEYWORD_VAR && (p)->tok.tok != TOK_KEYWORD_LET && + (p)->tok.tok != TOK_KEYWORD_CONST) + SYNTAX_ERROR(p); + else + pnext1(p); for(;;) { struct tok tmp = p->tok; EXPECT(p, TOK_IDENT); @@ -910,6 +916,8 @@ static mjs_err_t parse_statement(struct pstate* p) { pnext1(p); return MJS_OK; case TOK_KEYWORD_LET: + case TOK_KEYWORD_VAR: + case TOK_KEYWORD_CONST: return parse_let(p); case TOK_OPEN_CURLY: return parse_block(p, 1); @@ -939,7 +947,6 @@ static mjs_err_t parse_statement(struct pstate* p) { case TOK_KEYWORD_SWITCH: case TOK_KEYWORD_THROW: case TOK_KEYWORD_TRY: - case TOK_KEYWORD_VAR: case TOK_KEYWORD_VOID: case TOK_KEYWORD_WITH: mjs_set_errorf( diff --git a/lib/mjs/mjs_tok.c b/lib/mjs/mjs_tok.c index bdff5a86a..f89606d23 100644 --- a/lib/mjs/mjs_tok.c +++ b/lib/mjs/mjs_tok.c @@ -80,12 +80,13 @@ static int getnum(struct pstate* p) { } static int is_reserved_word_token(const char* s, int len) { - const char* reserved[] = {"break", "case", "catch", "continue", "debugger", "default", - "delete", "do", "else", "false", "finally", "for", - "function", "if", "in", "instanceof", "new", "null", - "return", "switch", "this", "throw", "true", "try", - "typeof", "var", "void", "while", "with", "let", - "undefined", NULL}; + const char* reserved[] = {"break", "case", "catch", "continue", "debugger", + "default", "delete", "do", "else", "false", + "finally", "for", "function", "if", "in", + "instanceof", "new", "null", "return", "switch", + "this", "throw", "true", "try", "typeof", + "var", "void", "while", "with", "let", + "const", "undefined", NULL}; int i; if(!mjs_is_alpha(s[0])) return 0; for(i = 0; reserved[i] != NULL; i++) { @@ -95,7 +96,8 @@ static int is_reserved_word_token(const char* s, int len) { } static int getident(struct pstate* p) { - while(mjs_is_ident(p->pos[0]) || mjs_is_digit(p->pos[0])) p->pos++; + while(mjs_is_ident(p->pos[0]) || mjs_is_digit(p->pos[0])) + p->pos++; p->tok.len = p->pos - p->tok.ptr; p->pos--; return TOK_IDENT; @@ -125,7 +127,8 @@ static void skip_spaces_and_comments(struct pstate* p) { p->pos++; } if(p->pos[0] == '/' && p->pos[1] == '/') { - while(p->pos[0] != '\0' && p->pos[0] != '\n') p->pos++; + while(p->pos[0] != '\0' && p->pos[0] != '\n') + p->pos++; } if(p->pos[0] == '/' && p->pos[1] == '*') { p->pos += 2; @@ -142,8 +145,8 @@ static void skip_spaces_and_comments(struct pstate* p) { } static int ptranslate(int tok) { -#define DT(a, b) ((a) << 8 | (b)) -#define TT(a, b, c) ((a) << 16 | (b) << 8 | (c)) +#define DT(a, b) ((a) << 8 | (b)) +#define TT(a, b, c) ((a) << 16 | (b) << 8 | (c)) #define QT(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d)) /* Map token ID produced by mjs_tok.c to token ID produced by lemon */ /* clang-format off */ diff --git a/lib/mjs/mjs_tok.h b/lib/mjs/mjs_tok.h index 03d8fe6fa..5ff079492 100644 --- a/lib/mjs/mjs_tok.h +++ b/lib/mjs/mjs_tok.h @@ -125,6 +125,7 @@ enum { TOK_KEYWORD_WHILE, TOK_KEYWORD_WITH, TOK_KEYWORD_LET, + TOK_KEYWORD_CONST, TOK_KEYWORD_UNDEFINED, TOK_MAX }; diff --git a/tsconfig.json b/tsconfig.json index 2655a8b97..53f0a3625 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,13 @@ "checkJs": true, "module": "CommonJS", "typeRoots": [ - "./applications/system/js_app/types" + "./applications/system/js_app/packages/fz-sdk/" ], "noLib": true, }, "include": [ "./applications/system/js_app/examples/apps/Scripts", "./applications/debug/unit_tests/resources/unit_tests/js", - "./applications/system/js_app/types/global.d.ts", + "./applications/system/js_app/packages/fz-sdk/global.d.ts", ] } \ No newline at end of file