Просмотр исходного кода

Add ir_remote from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: ir_remote
git-subtree-mainline: 7630b45810b988713dadc4c8ceebb5d4182c317f
git-subtree-split: b9a505f2ce77ef144b504b2842ed4a4ca49f4037
Willy-JL 1 год назад
Родитель
Сommit
e030e184de

+ 1 - 0
ir_remote/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/ir_remote

+ 63 - 0
ir_remote/README.md

@@ -0,0 +1,63 @@
+# Alternative Infrared Remote for Flipperzero
+
+It is a plugin like [UniversalRF Remix](https://github.com/ESurge/flipperzero-firmware-unirfremix) but for infrared files. I do this plugin for convenience, because the main IR app need to navigate for different button abit troublesome (buttons like up,down,left,right,back). I found it useful for TV and TV box.
+
+It supports short press and long press input for different ir remote buttons. Tested on the [unleashed firmware version unlshd-023](https://github.com/DarkFlippers/unleashed-firmware/releases/tag/unlshd-023)
+
+
+## How to install
+
+1. Update unleashed firmware to the version unlshd-023, then download the `ir_remote.fap` from [releases](https://github.com/Hong5489/ir_remote/tags)
+
+2. Put the `ir_remote.fap` file in your flipper's SD card, under `apps` folder
+
+## How to use
+
+1. Similar to UniRF app, put the path of the ir file and the ir button for each button on flipper (UP,DOWN,LEFT,RIGHT,BACK)
+
+The format With `HOLD` one is long press, without is short press
+
+Example of the configuration file:
+```
+REMOTE: /ext/infrared/Philips_32PFL4208T.ir
+UP: Up
+DOWN: Down
+LEFT: Left
+RIGHT: Right
+OK: 
+BACK: Back
+UPHOLD: VOL+
+DOWNHOLD: VOL-
+LEFTHOLD: Source
+RIGHTHOLD: SmartTV
+OKHOLD: POWER
+```
+
+Leave it empty for the button you don't need
+
+2. Save it as `.txt` file, then create a new folder in your SD card `ir_remote`, put it inside the folder
+
+3. Lastly, you can open the app, choose the configuration file, then you can try out the ir for each buttons
+
+4. Long press back button to exit the app
+
+## How to build 
+
+You can clone this repo and put it inside the `applications_user` folder, then build it with the command:
+```
+./fbt fap_ir_remote
+```
+Or you can build and run it on your flipper with the command:
+```
+./fbt launch_app APPSRC=applications_user/ir_remote
+```
+
+## Screenshots
+
+Choose config file to map
+
+![image](ir.png)
+
+Show all button name in the config file (If empty will show N/A). Upper part short press, Lower part long press
+
+![image2](ir2.png)

+ 18 - 0
ir_remote/application.fam

@@ -0,0 +1,18 @@
+App(
+    appid="ir_remote",
+    name="IR Remote",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="infrared_remote_app",
+    stack_size=3 * 1024,
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    fap_category="Infrared",
+    fap_icon="ir_10px.png",
+    fap_icon_assets="images",
+    fap_author="@Hong5489 & @friebel & @d4ve10",
+    fap_weburl="https://github.com/Hong5489/ir_remote",
+    fap_version="1.0",
+    fap_description="Bind any IR remote button to each button on flipper d-pad, provides another way to use flipper as IR remote.",
+)

+ 12 - 0
ir_remote/example.txt

@@ -0,0 +1,12 @@
+REMOTE: /ext/infrared/Philips_32PFL4208T.ir
+UP: Up
+DOWN: Down
+LEFT: Left
+RIGHT: Right
+OK: 
+BACK: Back
+UPHOLD: VOL+
+DOWNHOLD: VOL-
+LEFTHOLD: Source
+RIGHTHOLD: SmartTV
+OKHOLD: POWER

BIN
ir_remote/images/ButtonDown_7x4.png


BIN
ir_remote/images/ButtonLeft_4x7.png


BIN
ir_remote/images/ButtonRight_4x7.png


BIN
ir_remote/images/ButtonUp_7x4.png


BIN
ir_remote/images/Ok_btn_9x9.png


BIN
ir_remote/images/back_10px.png


BIN
ir_remote/images/sub1_10px.png


+ 188 - 0
ir_remote/infrared_remote.c

@@ -0,0 +1,188 @@
+#include "infrared_remote.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <m-array.h>
+#include <toolbox/path.h>
+#include <storage/storage.h>
+#include <core/common_defines.h>
+
+#define TAG "InfraredRemote"
+
+ARRAY_DEF(InfraredButtonArray, InfraredRemoteButton*, M_PTR_OPLIST);
+
+struct InfraredRemote {
+    InfraredButtonArray_t buttons;
+    FuriString* name;
+    FuriString* path;
+};
+
+static void infrared_remote_clear_buttons(InfraredRemote* remote) {
+    InfraredButtonArray_it_t it;
+    for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it);
+        InfraredButtonArray_next(it)) {
+        infrared_remote_button_free(*InfraredButtonArray_cref(it));
+    }
+    InfraredButtonArray_reset(remote->buttons);
+}
+
+InfraredRemote* infrared_remote_alloc(void) {
+    InfraredRemote* remote = malloc(sizeof(InfraredRemote));
+    InfraredButtonArray_init(remote->buttons);
+    remote->name = furi_string_alloc();
+    remote->path = furi_string_alloc();
+    return remote;
+}
+
+void infrared_remote_free(InfraredRemote* remote) {
+    infrared_remote_clear_buttons(remote);
+    InfraredButtonArray_clear(remote->buttons);
+    furi_string_free(remote->path);
+    furi_string_free(remote->name);
+    free(remote);
+}
+
+void infrared_remote_reset(InfraredRemote* remote) {
+    infrared_remote_clear_buttons(remote);
+    furi_string_reset(remote->name);
+    furi_string_reset(remote->path);
+}
+
+void infrared_remote_set_name(InfraredRemote* remote, const char* name) {
+    furi_string_set(remote->name, name);
+}
+
+const char* infrared_remote_get_name(InfraredRemote* remote) {
+    return furi_string_get_cstr(remote->name);
+}
+
+void infrared_remote_set_path(InfraredRemote* remote, const char* path) {
+    furi_string_set(remote->path, path);
+}
+
+const char* infrared_remote_get_path(InfraredRemote* remote) {
+    return furi_string_get_cstr(remote->path);
+}
+
+size_t infrared_remote_get_button_count(InfraredRemote* remote) {
+    return InfraredButtonArray_size(remote->buttons);
+}
+
+InfraredRemoteButton* infrared_remote_get_button(InfraredRemote* remote, size_t index) {
+    furi_assert(index < InfraredButtonArray_size(remote->buttons));
+    return *InfraredButtonArray_get(remote->buttons, index);
+}
+
+bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* name, size_t* index) {
+    for(size_t i = 0; i < InfraredButtonArray_size(remote->buttons); i++) {
+        InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, i);
+        if(!strcmp(infrared_remote_button_get_name(button), name)) {
+            *index = i;
+            return true;
+        }
+    }
+    return false;
+}
+
+bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal) {
+    InfraredRemoteButton* button = infrared_remote_button_alloc();
+    infrared_remote_button_set_name(button, name);
+    infrared_remote_button_set_signal(button, signal);
+    InfraredButtonArray_push_back(remote->buttons, button);
+    return infrared_remote_store(remote);
+}
+
+bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index) {
+    furi_assert(index < InfraredButtonArray_size(remote->buttons));
+    InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, index);
+    infrared_remote_button_set_name(button, new_name);
+    return infrared_remote_store(remote);
+}
+
+bool infrared_remote_delete_button(InfraredRemote* remote, size_t index) {
+    furi_assert(index < InfraredButtonArray_size(remote->buttons));
+    InfraredRemoteButton* button;
+    InfraredButtonArray_pop_at(&button, remote->buttons, index);
+    infrared_remote_button_free(button);
+    return infrared_remote_store(remote);
+}
+
+bool infrared_remote_store(InfraredRemote* remote) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_file_alloc(storage);
+    const char* path = furi_string_get_cstr(remote->path);
+
+    FURI_LOG_I(TAG, "store file: \'%s\'", path);
+
+    bool success = flipper_format_file_open_always(ff, path) &&
+                   flipper_format_write_header_cstr(ff, "IR signals file", 1);
+    if(success) {
+        InfraredButtonArray_it_t it;
+        for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it);
+            InfraredButtonArray_next(it)) {
+            InfraredRemoteButton* button = *InfraredButtonArray_cref(it);
+            success = infrared_signal_save(
+                infrared_remote_button_get_signal(button),
+                ff,
+                infrared_remote_button_get_name(button));
+            if(!success) {
+                break;
+            }
+        }
+    }
+
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}
+
+bool infrared_remote_load(InfraredRemote* remote, FuriString* path) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+
+    FuriString* buf;
+    buf = furi_string_alloc();
+
+    FURI_LOG_I(TAG, "load file: \'%s\'", furi_string_get_cstr(path));
+    bool success = flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path));
+
+    if(success) {
+        uint32_t version;
+        success = flipper_format_read_header(ff, buf, &version) &&
+                  !furi_string_cmp(buf, "IR signals file") && (version == 1);
+    }
+
+    if(success) {
+        path_extract_filename(path, buf, true);
+        infrared_remote_clear_buttons(remote);
+        infrared_remote_set_name(remote, furi_string_get_cstr(buf));
+        infrared_remote_set_path(remote, furi_string_get_cstr(path));
+
+        for(bool can_read = true; can_read;) {
+            InfraredRemoteButton* button = infrared_remote_button_alloc();
+            can_read = infrared_signal_read(infrared_remote_button_get_signal(button), ff, buf);
+            if(can_read) {
+                infrared_remote_button_set_name(button, furi_string_get_cstr(buf));
+                InfraredButtonArray_push_back(remote->buttons, button);
+            } else {
+                infrared_remote_button_free(button);
+            }
+        }
+    }
+
+    furi_string_free(buf);
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}
+
+bool infrared_remote_remove(InfraredRemote* remote) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    FS_Error status = storage_common_remove(storage, furi_string_get_cstr(remote->path));
+    infrared_remote_reset(remote);
+
+    furi_record_close(RECORD_STORAGE);
+    return (status == FSE_OK || status == FSE_NOT_EXIST);
+}

+ 31 - 0
ir_remote/infrared_remote.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <stdbool.h>
+
+#include "infrared_remote_button.h"
+
+#define IR_REMOTE_PATH EXT_PATH("infrared/remote")
+
+typedef struct InfraredRemote InfraredRemote;
+
+InfraredRemote* infrared_remote_alloc(void);
+void infrared_remote_free(InfraredRemote* remote);
+void infrared_remote_reset(InfraredRemote* remote);
+
+void infrared_remote_set_name(InfraredRemote* remote, const char* name);
+const char* infrared_remote_get_name(InfraredRemote* remote);
+
+void infrared_remote_set_path(InfraredRemote* remote, const char* path);
+const char* infrared_remote_get_path(InfraredRemote* remote);
+
+size_t infrared_remote_get_button_count(InfraredRemote* remote);
+InfraredRemoteButton* infrared_remote_get_button(InfraredRemote* remote, size_t index);
+bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* name, size_t* index);
+
+bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal);
+bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index);
+bool infrared_remote_delete_button(InfraredRemote* remote, size_t index);
+
+bool infrared_remote_store(InfraredRemote* remote);
+bool infrared_remote_load(InfraredRemote* remote, FuriString* path);
+bool infrared_remote_remove(InfraredRemote* remote);

+ 610 - 0
ir_remote/infrared_remote_app.c

@@ -0,0 +1,610 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <infrared_worker.h>
+
+#include <gui/gui.h>
+#include <input/input.h>
+#include <dialogs/dialogs.h>
+#include <ir_remote_icons.h>
+
+#include <infrared/infrared_app.h>
+#include <toolbox/saved_struct.h>
+
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+
+#include "infrared_signal.h"
+#include "infrared_remote.h"
+#include "infrared_remote_button.h"
+#define TAG "ir_remote"
+
+#include <flipper_format/flipper_format.h>
+
+typedef struct {
+    int status;
+    ViewPort* view_port;
+    FuriString* up_button;
+    FuriString* down_button;
+    FuriString* left_button;
+    FuriString* right_button;
+    FuriString* ok_button;
+    FuriString* back_button;
+    FuriString* up_hold_button;
+    FuriString* down_hold_button;
+    FuriString* left_hold_button;
+    FuriString* right_hold_button;
+    FuriString* ok_hold_button;
+    InfraredWorker* infrared_worker;
+} IRApp;
+
+// Screen is 128x64 px
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    // Show config is incorrect when cannot read the remote file
+    // Showing button string in the screen, upper part is short press, lower part is long press
+    IRApp* app = ctx;
+    if(app->status) {
+        canvas_clear(canvas);
+        view_port_set_orientation(app->view_port, ViewPortOrientationHorizontal);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 62, 5, AlignCenter, AlignTop, "Config is incorrect.");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(canvas, 62, 30, AlignCenter, AlignTop, "Please configure map.");
+        canvas_draw_str_aligned(canvas, 62, 60, AlignCenter, AlignBottom, "Press Back to Exit.");
+    } else {
+        canvas_clear(canvas);
+        view_port_set_orientation(app->view_port, ViewPortOrientationVertical);
+        canvas_draw_icon(canvas, 1, 5, &I_ButtonUp_7x4);
+        canvas_draw_icon(canvas, 1, 15, &I_ButtonDown_7x4);
+        canvas_draw_icon(canvas, 2, 23, &I_ButtonLeft_4x7);
+        canvas_draw_icon(canvas, 2, 33, &I_ButtonRight_4x7);
+        canvas_draw_icon(canvas, 0, 42, &I_Ok_btn_9x9);
+        canvas_draw_icon(canvas, 0, 53, &I_back_10px);
+
+        //Labels
+        canvas_set_font(canvas, FontSecondary);
+
+        canvas_draw_str_aligned(
+            canvas, 32, 8, AlignCenter, AlignCenter, furi_string_get_cstr(app->up_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 18, AlignCenter, AlignCenter, furi_string_get_cstr(app->down_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 28, AlignCenter, AlignCenter, furi_string_get_cstr(app->left_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 38, AlignCenter, AlignCenter, furi_string_get_cstr(app->right_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 48, AlignCenter, AlignCenter, furi_string_get_cstr(app->ok_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 58, AlignCenter, AlignCenter, furi_string_get_cstr(app->back_button));
+
+        canvas_draw_line(canvas, 0, 65, 64, 65);
+
+        canvas_draw_icon(canvas, 1, 70, &I_ButtonUp_7x4);
+        canvas_draw_icon(canvas, 1, 80, &I_ButtonDown_7x4);
+        canvas_draw_icon(canvas, 2, 88, &I_ButtonLeft_4x7);
+        canvas_draw_icon(canvas, 2, 98, &I_ButtonRight_4x7);
+        canvas_draw_icon(canvas, 0, 107, &I_Ok_btn_9x9);
+        canvas_draw_icon(canvas, 0, 118, &I_back_10px);
+
+        canvas_draw_str_aligned(
+            canvas, 32, 73, AlignCenter, AlignCenter, furi_string_get_cstr(app->up_hold_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 83, AlignCenter, AlignCenter, furi_string_get_cstr(app->down_hold_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 93, AlignCenter, AlignCenter, furi_string_get_cstr(app->left_hold_button));
+        canvas_draw_str_aligned(
+            canvas,
+            32,
+            103,
+            AlignCenter,
+            AlignCenter,
+            furi_string_get_cstr(app->right_hold_button));
+        canvas_draw_str_aligned(
+            canvas, 32, 113, AlignCenter, AlignCenter, furi_string_get_cstr(app->ok_hold_button));
+        canvas_draw_str_aligned(canvas, 32, 123, AlignCenter, AlignCenter, "Exit App");
+    }
+}
+
+static void app_input_callback(InputEvent* input_event, void* ctx) {
+    furi_assert(ctx);
+
+    FuriMessageQueue* event_queue = ctx;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+int32_t infrared_remote_app(char* p) {
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    // App button string
+    IRApp* app = malloc(sizeof(IRApp));
+    app->up_button = furi_string_alloc();
+    app->down_button = furi_string_alloc();
+    app->left_button = furi_string_alloc();
+    app->right_button = furi_string_alloc();
+    app->ok_button = furi_string_alloc();
+    app->back_button = furi_string_alloc();
+    app->up_hold_button = furi_string_alloc();
+    app->down_hold_button = furi_string_alloc();
+    app->left_hold_button = furi_string_alloc();
+    app->right_hold_button = furi_string_alloc();
+    app->ok_hold_button = furi_string_alloc();
+    app->view_port = view_port_alloc();
+    app->infrared_worker = infrared_worker_alloc();
+
+    // Configure view port
+    view_port_draw_callback_set(app->view_port, app_draw_callback, app);
+    view_port_input_callback_set(app->view_port, app_input_callback, event_queue);
+
+    // Register view port in GUI
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, app->view_port, GuiLayerFullscreen);
+
+    InputEvent event;
+
+    FuriString* map_file = furi_string_alloc();
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_file_alloc(storage);
+    if(!storage_file_exists(storage, IR_REMOTE_PATH)) {
+        storage_common_mkdir(storage, IR_REMOTE_PATH); //Make Folder If dir not exist
+    }
+
+    bool res;
+    if(p && strlen(p)) {
+        furi_string_set(map_file, p);
+        res = true;
+    } else {
+        DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
+        DialogsFileBrowserOptions browser_options;
+        dialog_file_browser_set_basic_options(&browser_options, ".txt", &I_sub1_10px);
+        browser_options.base_path = IR_REMOTE_PATH;
+        furi_string_set(map_file, IR_REMOTE_PATH);
+        res = dialog_file_browser_show(dialogs, map_file, map_file, &browser_options);
+        furi_record_close(RECORD_DIALOGS);
+    }
+
+    // if user didn't choose anything, free everything and exit
+    if(!res) {
+        FURI_LOG_I(TAG, "exit");
+        flipper_format_free(ff);
+        furi_record_close(RECORD_STORAGE);
+
+        furi_string_free(app->up_button);
+        furi_string_free(app->down_button);
+        furi_string_free(app->left_button);
+        furi_string_free(app->right_button);
+        furi_string_free(app->ok_button);
+        furi_string_free(app->back_button);
+        furi_string_free(app->up_hold_button);
+        furi_string_free(app->down_hold_button);
+        furi_string_free(app->left_hold_button);
+        furi_string_free(app->right_hold_button);
+        furi_string_free(app->ok_hold_button);
+
+        view_port_enabled_set(app->view_port, false);
+        gui_remove_view_port(gui, app->view_port);
+        view_port_free(app->view_port);
+        free(app);
+        furi_message_queue_free(event_queue);
+
+        furi_record_close(RECORD_GUI);
+        return 255;
+    }
+
+    InfraredRemote* remote = infrared_remote_alloc();
+    FuriString* remote_path = furi_string_alloc();
+
+    InfraredSignal* up_signal = infrared_signal_alloc();
+    InfraredSignal* down_signal = infrared_signal_alloc();
+    InfraredSignal* left_signal = infrared_signal_alloc();
+    InfraredSignal* right_signal = infrared_signal_alloc();
+    InfraredSignal* ok_signal = infrared_signal_alloc();
+    InfraredSignal* back_signal = infrared_signal_alloc();
+    InfraredSignal* up_hold_signal = infrared_signal_alloc();
+    InfraredSignal* down_hold_signal = infrared_signal_alloc();
+    InfraredSignal* left_hold_signal = infrared_signal_alloc();
+    InfraredSignal* right_hold_signal = infrared_signal_alloc();
+    InfraredSignal* ok_hold_signal = infrared_signal_alloc();
+
+    InfraredSignal* active_signal = NULL;
+    bool is_transmitting = false;
+
+    bool up_enabled = false;
+    bool down_enabled = false;
+    bool left_enabled = false;
+    bool right_enabled = false;
+    bool ok_enabled = false;
+    bool back_enabled = false;
+    bool up_hold_enabled = false;
+    bool down_hold_enabled = false;
+    bool left_hold_enabled = false;
+    bool right_hold_enabled = false;
+    bool ok_hold_enabled = false;
+
+    if(!flipper_format_file_open_existing(ff, furi_string_get_cstr(map_file))) {
+        FURI_LOG_E(TAG, "Could not open MAP file %s", furi_string_get_cstr(map_file));
+        app->status = 1;
+    } else {
+        //Filename Assignment/Check Start
+
+        if(!flipper_format_read_string(ff, "REMOTE", remote_path)) {
+            FURI_LOG_E(TAG, "Could not read REMOTE string");
+            app->status = 1;
+        } else {
+            if(!infrared_remote_load(remote, remote_path)) {
+                FURI_LOG_E(TAG, "Could not load ir file: %s", furi_string_get_cstr(remote_path));
+                app->status = 1;
+            } else {
+                FURI_LOG_I(TAG, "Loaded REMOTE file: %s", furi_string_get_cstr(remote_path));
+            }
+        }
+
+        //assign variables to values within map file
+        //set missing filenames to N/A
+        //assign button signals
+        size_t index = 0;
+        if(!flipper_format_read_string(ff, "UP", app->up_button)) {
+            FURI_LOG_W(TAG, "Could not read UP string");
+            furi_string_set(app->up_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->up_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                up_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                up_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "DOWN", app->down_button)) {
+            FURI_LOG_W(TAG, "Could not read DOWN string");
+            furi_string_set(app->down_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->down_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                down_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                down_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "LEFT", app->left_button)) {
+            FURI_LOG_W(TAG, "Could not read LEFT string");
+            furi_string_set(app->left_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->left_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                left_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                left_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "RIGHT", app->right_button)) {
+            FURI_LOG_W(TAG, "Could not read RIGHT string");
+            furi_string_set(app->right_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->right_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                right_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                right_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "OK", app->ok_button)) {
+            FURI_LOG_W(TAG, "Could not read OK string");
+            furi_string_set(app->ok_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->ok_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                ok_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                ok_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "BACK", app->back_button)) {
+            FURI_LOG_W(TAG, "Could not read BACK string");
+            furi_string_set(app->back_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->back_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                back_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                back_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "UPHOLD", app->up_hold_button)) {
+            FURI_LOG_W(TAG, "Could not read UPHOLD string");
+            furi_string_set(app->up_hold_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->up_hold_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                up_hold_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                up_hold_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "DOWNHOLD", app->down_hold_button)) {
+            FURI_LOG_W(TAG, "Could not read DOWNHOLD string");
+            furi_string_set(app->down_hold_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->down_hold_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                down_hold_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                down_hold_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "LEFTHOLD", app->left_hold_button)) {
+            FURI_LOG_W(TAG, "Could not read LEFTHOLD string");
+            furi_string_set(app->left_hold_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->left_hold_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                left_hold_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                left_hold_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "RIGHTHOLD", app->right_hold_button)) {
+            FURI_LOG_W(TAG, "Could not read RIGHTHOLD string");
+            furi_string_set(app->right_hold_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->right_hold_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                right_hold_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                right_hold_enabled = true;
+            }
+        }
+
+        if(!flipper_format_read_string(ff, "OKHOLD", app->ok_hold_button)) {
+            FURI_LOG_W(TAG, "Could not read OKHOLD string");
+            furi_string_set(app->ok_hold_button, "N/A");
+        } else {
+            if(!infrared_remote_find_button_by_name(
+                   remote, furi_string_get_cstr(app->ok_hold_button), &index)) {
+                FURI_LOG_W(TAG, "Error");
+            } else {
+                ok_hold_signal =
+                    infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
+                ok_hold_enabled = true;
+            }
+        }
+    }
+
+    furi_string_free(remote_path);
+
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+
+    bool otg_was_enabled = furi_hal_power_is_otg_enabled();
+    InfraredSettings settings = {0};
+    saved_struct_load(
+        INFRARED_SETTINGS_PATH,
+        &settings,
+        sizeof(InfraredSettings),
+        INFRARED_SETTINGS_MAGIC,
+        INFRARED_SETTINGS_VERSION);
+    if(settings.tx_pin < FuriHalInfraredTxPinMax) {
+        furi_hal_infrared_set_tx_output(settings.tx_pin);
+        if(settings.otg_enabled != otg_was_enabled) {
+            if(settings.otg_enabled) {
+                furi_hal_power_enable_otg();
+            } else {
+                furi_hal_power_disable_otg();
+            }
+        }
+    } else {
+        FuriHalInfraredTxPin tx_pin_detected = furi_hal_infrared_detect_tx_output();
+        furi_hal_infrared_set_tx_output(tx_pin_detected);
+        if(tx_pin_detected != FuriHalInfraredTxPinInternal) {
+            furi_hal_power_enable_otg();
+        }
+    }
+
+    bool running = true;
+    NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+
+    if(app->status) {
+        view_port_update(app->view_port);
+        while(running) {
+            if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
+                if(event.type == InputTypeShort) {
+                    switch(event.key) {
+                    case InputKeyBack:
+                        running = false;
+                        break;
+                    default:
+                        break;
+                    }
+                }
+            }
+        }
+    } else {
+        view_port_update(app->view_port);
+        while(running) {
+            if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
+                // short press signal
+                if(event.type == InputTypeShort) {
+                    switch(event.key) {
+                    case InputKeyUp:
+                        if(up_enabled) {
+                            active_signal = up_signal;
+                            FURI_LOG_I(TAG, "up");
+                        }
+                        break;
+                    case InputKeyDown:
+                        if(down_enabled) {
+                            active_signal = down_signal;
+                            FURI_LOG_I(TAG, "down");
+                        }
+                        break;
+                    case InputKeyRight:
+                        if(right_enabled) {
+                            active_signal = right_signal;
+                            FURI_LOG_I(TAG, "right");
+                        }
+                        break;
+                    case InputKeyLeft:
+                        if(left_enabled) {
+                            active_signal = left_signal;
+                            FURI_LOG_I(TAG, "left");
+                        }
+                        break;
+                    case InputKeyOk:
+                        if(ok_enabled) {
+                            active_signal = ok_signal;
+                            FURI_LOG_I(TAG, "ok");
+                        }
+                        break;
+                    case InputKeyBack:
+                        if(back_enabled) {
+                            active_signal = back_signal;
+                            FURI_LOG_I(TAG, "back");
+                        }
+                        break;
+                    default:
+                        running = false;
+                        break;
+                    }
+                    // long press signal
+                } else if(event.type == InputTypeLong) {
+                    switch(event.key) {
+                    case InputKeyUp:
+                        if(up_hold_enabled) {
+                            active_signal = up_hold_signal;
+                            FURI_LOG_I(TAG, "up!");
+                        }
+                        break;
+                    case InputKeyDown:
+                        if(down_hold_enabled) {
+                            active_signal = down_hold_signal;
+                            FURI_LOG_I(TAG, "down!");
+                        }
+                        break;
+                    case InputKeyRight:
+                        if(right_hold_enabled) {
+                            active_signal = right_hold_signal;
+                            FURI_LOG_I(TAG, "right!");
+                        }
+                        break;
+                    case InputKeyLeft:
+                        if(left_hold_enabled) {
+                            active_signal = left_hold_signal;
+                            FURI_LOG_I(TAG, "left!");
+                        }
+                        break;
+                    case InputKeyOk:
+                        if(ok_hold_enabled) {
+                            active_signal = ok_hold_signal;
+                            FURI_LOG_I(TAG, "ok!");
+                        }
+                        break;
+                    default:
+                        running = false;
+                        break;
+                    }
+                } else if(event.type == InputTypeRelease && is_transmitting) {
+                    notification_message(notification, &sequence_blink_stop);
+                    infrared_worker_tx_stop(app->infrared_worker);
+                    is_transmitting = false;
+                    active_signal = NULL;
+                }
+
+                if(active_signal != NULL &&
+                   (event.type == InputTypeShort || event.type == InputTypeLong)) {
+                    if(is_transmitting) {
+                        infrared_worker_tx_stop(app->infrared_worker);
+                    }
+
+                    if(infrared_signal_is_raw(active_signal)) {
+                        InfraredRawSignal* raw_signal =
+                            infrared_signal_get_raw_signal(active_signal);
+                        infrared_worker_set_raw_signal(
+                            app->infrared_worker,
+                            raw_signal->timings,
+                            raw_signal->timings_size,
+                            raw_signal->frequency,
+                            raw_signal->duty_cycle);
+                    } else {
+                        InfraredMessage* message = infrared_signal_get_message(active_signal);
+                        infrared_worker_set_decoded_signal(app->infrared_worker, message);
+                    }
+
+                    infrared_worker_tx_set_get_signal_callback(
+                        app->infrared_worker, infrared_worker_tx_get_signal_steady_callback, app);
+
+                    infrared_worker_tx_start(app->infrared_worker);
+                    notification_message(notification, &sequence_blink_start_magenta);
+                    is_transmitting = true;
+                }
+            }
+            view_port_update(app->view_port);
+        }
+    }
+
+    furi_hal_infrared_set_tx_output(FuriHalInfraredTxPinInternal);
+    if(furi_hal_power_is_otg_enabled() != otg_was_enabled) {
+        if(otg_was_enabled) {
+            furi_hal_power_enable_otg();
+        } else {
+            furi_hal_power_disable_otg();
+        }
+    }
+
+    // Free all things
+    furi_string_free(app->up_button);
+    furi_string_free(app->down_button);
+    furi_string_free(app->left_button);
+    furi_string_free(app->right_button);
+    furi_string_free(app->ok_button);
+    furi_string_free(app->back_button);
+    furi_string_free(app->up_hold_button);
+    furi_string_free(app->down_hold_button);
+    furi_string_free(app->left_hold_button);
+    furi_string_free(app->right_hold_button);
+    furi_string_free(app->ok_hold_button);
+
+    if(is_transmitting) {
+        infrared_worker_tx_stop(app->infrared_worker);
+        notification_message(notification, &sequence_blink_stop);
+    }
+    infrared_worker_free(app->infrared_worker);
+
+    infrared_remote_free(remote);
+    view_port_enabled_set(app->view_port, false);
+    gui_remove_view_port(gui, app->view_port);
+    view_port_free(app->view_port);
+    free(app);
+    furi_message_queue_free(event_queue);
+
+    furi_record_close(RECORD_NOTIFICATION);
+    furi_record_close(RECORD_GUI);
+
+    return 0;
+}

+ 37 - 0
ir_remote/infrared_remote_button.c

@@ -0,0 +1,37 @@
+#include "infrared_remote_button.h"
+
+#include <stdlib.h>
+
+struct InfraredRemoteButton {
+    FuriString* name;
+    InfraredSignal* signal;
+};
+
+InfraredRemoteButton* infrared_remote_button_alloc(void) {
+    InfraredRemoteButton* button = malloc(sizeof(InfraredRemoteButton));
+    button->name = furi_string_alloc();
+    button->signal = infrared_signal_alloc();
+    return button;
+}
+
+void infrared_remote_button_free(InfraredRemoteButton* button) {
+    furi_string_free(button->name);
+    infrared_signal_free(button->signal);
+    free(button);
+}
+
+void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name) {
+    furi_string_set(button->name, name);
+}
+
+const char* infrared_remote_button_get_name(InfraredRemoteButton* button) {
+    return furi_string_get_cstr(button->name);
+}
+
+void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal) {
+    infrared_signal_set_signal(button->signal, signal);
+}
+
+InfraredSignal* infrared_remote_button_get_signal(InfraredRemoteButton* button) {
+    return button->signal;
+}

+ 14 - 0
ir_remote/infrared_remote_button.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "infrared_signal.h"
+
+typedef struct InfraredRemoteButton InfraredRemoteButton;
+
+InfraredRemoteButton* infrared_remote_button_alloc(void);
+void infrared_remote_button_free(InfraredRemoteButton* button);
+
+void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name);
+const char* infrared_remote_button_get_name(InfraredRemoteButton* button);
+
+void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal);
+InfraredSignal* infrared_remote_button_get_signal(InfraredRemoteButton* button);

+ 300 - 0
ir_remote/infrared_signal.c

@@ -0,0 +1,300 @@
+#include "infrared_signal.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <core/check.h>
+#include <lib/infrared/worker/infrared_transmit.h>
+#include <lib/infrared/worker/infrared_worker.h>
+
+#define TAG "InfraredSignal"
+
+struct InfraredSignal {
+    bool is_raw;
+    union {
+        InfraredMessage message;
+        InfraredRawSignal raw;
+    } payload;
+};
+
+static void infrared_signal_clear_timings(InfraredSignal* signal) {
+    if(signal->is_raw) {
+        free(signal->payload.raw.timings);
+        signal->payload.raw.timings_size = 0;
+        signal->payload.raw.timings = NULL;
+    }
+}
+
+static bool infrared_signal_is_message_valid(InfraredMessage* message) {
+    if(!infrared_is_protocol_valid(message->protocol)) {
+        FURI_LOG_E(TAG, "Unknown protocol");
+        return false;
+    }
+
+    uint32_t address_length = infrared_get_protocol_address_length(message->protocol);
+    uint32_t address_mask = (1UL << address_length) - 1;
+
+    if(message->address != (message->address & address_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Address is out of range (mask 0x%08lX): 0x%lX\r\n",
+            address_mask,
+            message->address);
+        return false;
+    }
+
+    uint32_t command_length = infrared_get_protocol_command_length(message->protocol);
+    uint32_t command_mask = (1UL << command_length) - 1;
+
+    if(message->command != (message->command & command_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Command is out of range (mask 0x%08lX): 0x%lX\r\n",
+            command_mask,
+            message->command);
+        return false;
+    }
+
+    return true;
+}
+
+static bool infrared_signal_is_raw_valid(InfraredRawSignal* raw) {
+    if((raw->frequency > INFRARED_MAX_FREQUENCY) || (raw->frequency < INFRARED_MIN_FREQUENCY)) {
+        FURI_LOG_E(
+            TAG,
+            "Frequency is out of range (%X - %X): %lX",
+            INFRARED_MIN_FREQUENCY,
+            INFRARED_MAX_FREQUENCY,
+            raw->frequency);
+        return false;
+
+    } else if((raw->duty_cycle <= 0) || (raw->duty_cycle > 1)) {
+        FURI_LOG_E(TAG, "Duty cycle is out of range (0 - 1): %f", (double)raw->duty_cycle);
+        return false;
+
+    } else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) {
+        FURI_LOG_E(
+            TAG,
+            "Timings amount is out of range (0 - %X): %X",
+            MAX_TIMINGS_AMOUNT,
+            raw->timings_size);
+        return false;
+    }
+
+    return true;
+}
+
+static inline bool infrared_signal_save_message(InfraredMessage* message, FlipperFormat* ff) {
+    const char* protocol_name = infrared_get_protocol_name(message->protocol);
+    return flipper_format_write_string_cstr(ff, "type", "parsed") &&
+           flipper_format_write_string_cstr(ff, "protocol", protocol_name) &&
+           flipper_format_write_hex(ff, "address", (uint8_t*)&message->address, 4) &&
+           flipper_format_write_hex(ff, "command", (uint8_t*)&message->command, 4);
+}
+
+static inline bool infrared_signal_save_raw(InfraredRawSignal* raw, FlipperFormat* ff) {
+    furi_assert(raw->timings_size <= MAX_TIMINGS_AMOUNT);
+    return flipper_format_write_string_cstr(ff, "type", "raw") &&
+           flipper_format_write_uint32(ff, "frequency", &raw->frequency, 1) &&
+           flipper_format_write_float(ff, "duty_cycle", &raw->duty_cycle, 1) &&
+           flipper_format_write_uint32(ff, "data", raw->timings, raw->timings_size);
+}
+
+static inline bool infrared_signal_read_message(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* buf;
+    buf = furi_string_alloc();
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "protocol", buf)) break;
+
+        InfraredMessage message;
+        message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
+
+        success = flipper_format_read_hex(ff, "address", (uint8_t*)&message.address, 4) &&
+                  flipper_format_read_hex(ff, "command", (uint8_t*)&message.command, 4) &&
+                  infrared_signal_is_message_valid(&message);
+
+        if(!success) break;
+
+        infrared_signal_set_message(signal, &message);
+    } while(0);
+
+    furi_string_free(buf);
+    return success;
+}
+
+static inline bool infrared_signal_read_raw(InfraredSignal* signal, FlipperFormat* ff) {
+    uint32_t timings_size, frequency;
+    float duty_cycle;
+
+    bool success = flipper_format_read_uint32(ff, "frequency", &frequency, 1) &&
+                   flipper_format_read_float(ff, "duty_cycle", &duty_cycle, 1) &&
+                   flipper_format_get_value_count(ff, "data", &timings_size);
+
+    if(!success || timings_size > MAX_TIMINGS_AMOUNT) {
+        return false;
+    }
+
+    uint32_t* timings = malloc(sizeof(uint32_t) * timings_size);
+    success = flipper_format_read_uint32(ff, "data", timings, timings_size);
+
+    if(success) {
+        infrared_signal_set_raw_signal(signal, timings, timings_size, frequency, duty_cycle);
+    }
+
+    free(timings);
+    return success;
+}
+
+static bool infrared_signal_read_body(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "type", tmp)) break;
+        if(furi_string_equal(tmp, "raw")) {
+            success = infrared_signal_read_raw(signal, ff);
+        } else if(furi_string_equal(tmp, "parsed")) {
+            success = infrared_signal_read_message(signal, ff);
+        } else {
+            FURI_LOG_E(TAG, "Unknown signal type");
+        }
+    } while(false);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+InfraredSignal* infrared_signal_alloc(void) {
+    InfraredSignal* signal = malloc(sizeof(InfraredSignal));
+
+    signal->is_raw = false;
+    signal->payload.message.protocol = InfraredProtocolUnknown;
+
+    return signal;
+}
+
+void infrared_signal_free(InfraredSignal* signal) {
+    infrared_signal_clear_timings(signal);
+    free(signal);
+}
+
+bool infrared_signal_is_raw(InfraredSignal* signal) {
+    return signal->is_raw;
+}
+
+bool infrared_signal_is_valid(InfraredSignal* signal) {
+    return signal->is_raw ? infrared_signal_is_raw_valid(&signal->payload.raw) :
+                            infrared_signal_is_message_valid(&signal->payload.message);
+}
+
+void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other) {
+    if(other->is_raw) {
+        const InfraredRawSignal* raw = &other->payload.raw;
+        infrared_signal_set_raw_signal(
+            signal, raw->timings, raw->timings_size, raw->frequency, raw->duty_cycle);
+    } else {
+        const InfraredMessage* message = &other->payload.message;
+        infrared_signal_set_message(signal, message);
+    }
+}
+
+void infrared_signal_set_raw_signal(
+    InfraredSignal* signal,
+    const uint32_t* timings,
+    size_t timings_size,
+    uint32_t frequency,
+    float duty_cycle) {
+    infrared_signal_clear_timings(signal);
+
+    signal->is_raw = true;
+
+    signal->payload.raw.timings_size = timings_size;
+    signal->payload.raw.frequency = frequency;
+    signal->payload.raw.duty_cycle = duty_cycle;
+
+    signal->payload.raw.timings = malloc(timings_size * sizeof(uint32_t));
+    memcpy(signal->payload.raw.timings, timings, timings_size * sizeof(uint32_t));
+}
+
+InfraredRawSignal* infrared_signal_get_raw_signal(InfraredSignal* signal) {
+    furi_assert(signal->is_raw);
+    return &signal->payload.raw;
+}
+
+void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message) {
+    infrared_signal_clear_timings(signal);
+
+    signal->is_raw = false;
+    signal->payload.message = *message;
+}
+
+InfraredMessage* infrared_signal_get_message(InfraredSignal* signal) {
+    furi_assert(!signal->is_raw);
+    return &signal->payload.message;
+}
+
+bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name) {
+    if(!flipper_format_write_comment_cstr(ff, "") ||
+       !flipper_format_write_string_cstr(ff, "name", name)) {
+        return false;
+    } else if(signal->is_raw) {
+        return infrared_signal_save_raw(&signal->payload.raw, ff);
+    } else {
+        return infrared_signal_save_message(&signal->payload.message, ff);
+    }
+}
+
+bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "name", tmp)) break;
+        furi_string_set(name, tmp);
+        if(!infrared_signal_read_body(signal, ff)) break;
+        success = true;
+    } while(0);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+bool infrared_signal_search_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const FuriString* name) {
+    bool success = false;
+    FuriString* tmp = furi_string_alloc();
+
+    do {
+        bool is_name_found = false;
+        while(flipper_format_read_string(ff, "name", tmp)) {
+            is_name_found = furi_string_equal(name, tmp);
+            if(is_name_found) break;
+        }
+        if(!is_name_found) break;
+        if(!infrared_signal_read_body(signal, ff)) break;
+        success = true;
+    } while(false);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+void infrared_signal_transmit(InfraredSignal* signal) {
+    if(signal->is_raw) {
+        InfraredRawSignal* raw_signal = &signal->payload.raw;
+        infrared_send_raw_ext(
+            raw_signal->timings,
+            raw_signal->timings_size,
+            true,
+            raw_signal->frequency,
+            raw_signal->duty_cycle);
+    } else {
+        InfraredMessage* message = &signal->payload.message;
+        infrared_send(message, 2);
+    }
+}

+ 45 - 0
ir_remote/infrared_signal.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <lib/infrared/encoder_decoder/infrared.h>
+#include <flipper_format/flipper_format.h>
+
+typedef struct InfraredSignal InfraredSignal;
+
+typedef struct {
+    size_t timings_size;
+    uint32_t* timings;
+    uint32_t frequency;
+    float duty_cycle;
+} InfraredRawSignal;
+
+InfraredSignal* infrared_signal_alloc(void);
+void infrared_signal_free(InfraredSignal* signal);
+
+bool infrared_signal_is_raw(InfraredSignal* signal);
+bool infrared_signal_is_valid(InfraredSignal* signal);
+
+void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other);
+
+void infrared_signal_set_raw_signal(
+    InfraredSignal* signal,
+    const uint32_t* timings,
+    size_t timings_size,
+    uint32_t frequency,
+    float duty_cycle);
+InfraredRawSignal* infrared_signal_get_raw_signal(InfraredSignal* signal);
+
+void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message);
+InfraredMessage* infrared_signal_get_message(InfraredSignal* signal);
+
+bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name);
+bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name);
+bool infrared_signal_search_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const FuriString* name);
+
+void infrared_signal_transmit(InfraredSignal* signal);

BIN
ir_remote/ir.png


BIN
ir_remote/ir2.png


BIN
ir_remote/ir_10px.png