From 3872c7a73d85ae0b7c0658c691098d0fea8e4ced Mon Sep 17 00:00:00 2001 From: Luis Mayo Valbuena Date: Sun, 1 Feb 2026 14:31:31 +0100 Subject: [PATCH] feat: add IR capabilities to the JS engine --- applications/system/js_app/application.fam | 8 ++ .../apps/Scripts/js_examples/infrared-send.js | 47 +++++++ applications/system/js_app/js_modules.c | 1 + .../js_app/modules/js_infrared/js_infrared.c | 129 ++++++++++++++++++ .../packages/fz-sdk/infrared/index.d.ts | 41 ++++++ 5 files changed, 226 insertions(+) create mode 100644 applications/system/js_app/examples/apps/Scripts/js_examples/infrared-send.js create mode 100644 applications/system/js_app/modules/js_infrared/js_infrared.c create mode 100644 applications/system/js_app/packages/fz-sdk/infrared/index.d.ts diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index abafb6ea0..baef951e8 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -240,6 +240,14 @@ App( sources=["modules/js_subghz/*.c"], ) +App( + appid="js_infrared", + apptype=FlipperAppType.PLUGIN, + entry_point="js_infrared_ep", + requires=["js_app"], + sources=["modules/js_infrared/*.c"], +) + App( appid="js_blebeacon", apptype=FlipperAppType.PLUGIN, diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/infrared-send.js b/applications/system/js_app/examples/apps/Scripts/js_examples/infrared-send.js new file mode 100644 index 000000000..6606ef5f8 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/infrared-send.js @@ -0,0 +1,47 @@ +checkSdkFeatures(["infrared-send"]); +let infrared = require("infrared"); + +print("Sending Samsung32 signal (lowers volume)..."); +infrared.sendSignal("Samsung32", 0x00000007, 0x0000000b); +delay(1000); +print("Sending raw signal... (Fujitsu AC)"); +infrared.sendRawSignal( + [ + 3298, 1571, 442, 368, 442, 367, 443, 1180, 442, 370, 440, 1181, 442, 368, + 442, 367, 442, 368, 442, 1180, 443, 1180, 442, 368, 441, 367, 442, 369, 441, + 1180, 442, 1180, 443, 368, 442, 369, 441, 368, 442, 368, 442, 368, 442, 368, + 441, 368, 441, 368, 442, 368, 442, 368, 442, 367, 442, 368, 442, 366, 444, + 1181, 442, 368, 441, 367, 442, 368, 442, 367, 442, 369, 441, 367, 443, 367, + 442, 1180, 442, 368, 442, 368, 442, 368, 441, 368, 441, 1180, 442, 1180, + 442, 1181, 442, 1181, 441, 1181, 442, 1181, 442, 1181, 442, 1181, 442, 369, + 441, 368, 441, 1182, 442, 367, 443, 367, 443, 367, 442, 368, 442, 1181, 441, + 369, 441, 369, 441, 369, 441, 1180, 442, 1181, 441, 369, 442, 368, 442, 367, + 442, 368, 441, 1181, 441, 1183, 440, 368, 442, 368, 441, 369, 441, 1181, + 442, 368, 442, 367, 443, 1181, 442, 368, 442, 369, 441, 368, 441, 369, 440, + 368, 442, 367, 443, 367, 442, 367, 442, 367, 442, 368, 442, 369, 441, 369, + 441, 368, 442, 369, 441, 368, 441, 367, 443, 368, 442, 367, 442, 369, 441, + 369, 441, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 368, + 442, 368, 441, 367, 442, 367, 442, 368, 442, 368, 441, 368, 442, 369, 441, + 368, 441, 367, 442, 368, 441, 368, 442, 368, 442, 368, 442, 368, 442, 367, + 442, 1181, 442, 367, 442, 368, 441, 1181, 442, 1182, 441, 1181, 442, 1181, + 442, 1181, 441, 368, 442, 368, 442, 369, 441, + ], + true, + { frequency: 38000, dutyCycle: 0.33 }, +); +delay(1000); +print( + "Sending raw signal... (Fujitsu AC) with default frequency and duty cycle", +); +infrared.sendRawSignal([ + 3300, 1596, 416, 362, 448, 363, 446, 1177, 445, 363, 446, 1177, 445, 362, 448, + 362, 448, 364, 446, 1178, 444, 1207, 415, 362, 448, 362, 448, 363, 447, 1177, + 445, 1177, 446, 362, 448, 362, 447, 362, 447, 362, 448, 363, 447, 362, 447, + 363, 447, 363, 447, 363, 446, 363, 446, 362, 447, 362, 447, 363, 446, 1177, + 445, 363, 447, 364, 446, 362, 448, 363, 447, 363, 446, 362, 447, 362, 448, + 1175, 447, 363, 447, 364, 446, 362, 448, 362, 448, 1176, 446, 362, 448, 362, + 448, 363, 446, 362, 448, 362, 448, 363, 447, 1175, 446, 394, 415, 1176, 446, + 1178, 444, 1174, 449, 1177, 445, 1180, 443, 1179, 443, +]); + +print("Success"); diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 93c8b4af8..845a596ab 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -281,6 +281,7 @@ static const char* extra_features[] = { "blebeacon", "i2c", "spi", + "infrared-send", "subghz", "usbdisk", "vgm", diff --git a/applications/system/js_app/modules/js_infrared/js_infrared.c b/applications/system/js_app/modules/js_infrared/js_infrared.c new file mode 100644 index 000000000..9015a98ea --- /dev/null +++ b/applications/system/js_app/modules/js_infrared/js_infrared.c @@ -0,0 +1,129 @@ +#include "../../js_modules.h" +#include +#include +#include + +#define TAG "JsMath" + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +void js_send_protocol_signal(struct mjs* mjs) { + size_t num_args = mjs_nargs(mjs); + if(num_args < 3 || num_args > 4) { + ret_bad_args(mjs, "Wrong argument count"); + return; + } + if(!mjs_is_string(mjs_arg(mjs, 0)) || !mjs_is_number(mjs_arg(mjs, 1)) || + !mjs_is_number(mjs_arg(mjs, 2)) || (num_args == 4 && !mjs_is_object(mjs_arg(mjs, 3)))) { + ret_bad_args(mjs, "Wrong argument type"); + return; + } + bool repeat = false; + int times = 1; + if(num_args == 4) { + mjs_val_t options_obj = mjs_arg(mjs, 3); + + mjs_val_t repeat_val = mjs_get(mjs, options_obj, "repeat", ~0); + if(mjs_is_boolean(repeat_val)) { + repeat = mjs_get_bool(mjs, repeat_val); + } else if(!mjs_is_undefined(repeat_val)) { + ret_bad_args(mjs, "Wrong 'repeat' option type"); + return; + } + + mjs_val_t times_val = mjs_get(mjs, options_obj, "times", ~0); + if(mjs_is_number(times_val)) { + times = mjs_get_int(mjs, times_val); + } else if(!mjs_is_undefined(times_val)) { + ret_bad_args(mjs, "Wrong 'times' option type"); + return; + } + } + + InfraredMessage message; + message.repeat = repeat; + mjs_val_t protocol_arg = mjs_arg(mjs, 0); + message.protocol = infrared_get_protocol_by_name(mjs_get_cstring(mjs, &protocol_arg)); + message.address = mjs_get_int(mjs, mjs_arg(mjs, 1)); + message.command = mjs_get_int(mjs, mjs_arg(mjs, 2)); + infrared_send(&message, times); +} + +void js_send_raw_signal(struct mjs* mjs) { + size_t num_args = mjs_nargs(mjs); + if(num_args < 1 || num_args > 3) { + ret_bad_args(mjs, "Wrong argument count"); + return; + } + if(!mjs_is_array(mjs_arg(mjs, 0)) || (num_args > 1 && !mjs_is_boolean(mjs_arg(mjs, 1))) || + (num_args > 2 && !mjs_is_object(mjs_arg(mjs, 2)))) { + ret_bad_args(mjs, "Wrong argument type"); + return; + } + int array_length = mjs_array_length(mjs, mjs_arg(mjs, 0)); + uint32_t timings[array_length]; + for(int i = 0; i < array_length; i++) { + mjs_val_t elem = mjs_array_get(mjs, mjs_arg(mjs, 0), i); + if(!mjs_is_number(elem)) { + ret_bad_args(mjs, "Timings array must contain only numbers"); + return; + } + timings[i] = mjs_get_int(mjs, elem); + } + + bool start_from_mark = true; + if(num_args > 1) { + start_from_mark = mjs_get_bool(mjs, mjs_arg(mjs, 1)); + } + + if(num_args > 2) { + mjs_val_t options_obj = mjs_arg(mjs, 2); + + mjs_val_t frequency_val = mjs_get(mjs, options_obj, "frequency", ~0); + if(!mjs_is_number(frequency_val)) { + ret_bad_args(mjs, "Wrong 'frequency' option type"); + return; + } + + mjs_val_t duty_val = mjs_get(mjs, options_obj, "dutyCycle", ~0); + if(!mjs_is_number(duty_val)) { + ret_bad_args(mjs, "Wrong 'dutyCycle' option type"); + return; + } + uint32_t frequency = mjs_get_int(mjs, frequency_val); + float duty_cycle = mjs_get_double(mjs, duty_val); + infrared_send_raw_ext(timings, array_length, start_from_mark, frequency, duty_cycle); + } else { + infrared_send_raw(timings, array_length, start_from_mark); + } + return; +} + +static void* js_infrared_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + mjs_val_t infrared_object = mjs_mk_object(mjs); + mjs_set(mjs, infrared_object, "sendSignal", ~0, MJS_MK_FN(js_send_protocol_signal)); + mjs_set(mjs, infrared_object, "sendRawSignal", ~0, MJS_MK_FN(js_send_raw_signal)); + *object = infrared_object; + return (void*)1; +} + +static const JsModuleDescriptor js_infrared_desc = { + "infrared", + js_infrared_create, + NULL, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_infrared_desc, +}; + +const FlipperAppPluginDescriptor* js_infrared_ep(void) { + return &plugin_descriptor; +} diff --git a/applications/system/js_app/packages/fz-sdk/infrared/index.d.ts b/applications/system/js_app/packages/fz-sdk/infrared/index.d.ts new file mode 100644 index 000000000..4767167f1 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/infrared/index.d.ts @@ -0,0 +1,41 @@ +/** + * Module for using Infrared blaster/receptor + * @version Available with JS feature `infrared-send` + * @module + */ + +/** + * Sends an IR signal using a known protocol by Flipper Firmware + * @param address Note that the address expects a number. If you're reading from Flipper's IR files, the address is usually in little-endian hex format. Javascript numbers are defined as big endian by default. + * @param command Note that the command expects a number. If you're reading from Flipper's IR files, the command is usually in little-endian hex format. Javascript numbers are defined as big endian by default. + * @param options Repeat marks the signal as a repeat signal, times indicates how many times to send the signal + */ +export declare function sendSignal( + protocol: + | "NEC" + | "NECext" + | "NEC42" + | "NEC42ext" + | "Samsung32" + | "RC6" + | "RC5" + | "RC5X" + | "SIRC" + | "SIRC15" + | "SIRC20" + | "Kaseikyo" + | "RCA", + address: number, + command: number, + options?: { repeat?: boolean; times?: number }, +): void; + +/** + * Sends a signal from an unknown protocol + * @param startFromMark defaults to true + */ +export declare function sendRawSignal( + timings: number[], + startFromMark?: boolean, + advancedSettings?: { frequency: number; dutyCycle: number }, +): void;