فهرست منبع

Finished learn mode and signal analyzer (#3)

* Implemented signal receiver and learn mode view skeleton

* Receive and display infrared message in learn mode

* Fixed variable type

* Added buttons to received ir signal view

* Stable IR signal receiver and analyzer

* Stable signal handling and learn navigation skeleton

* Ask to enter new remote name when finishing learn

* Implemented exit dialog warning for learning mode

* Finished learn mode and signal analyzer

---------

Co-authored-by: Sandro Kalatozishvili <skalatozishvili@cloudlinux.com>
Sandro Kalatozishvili 2 سال پیش
والد
کامیت
a08a27baaf

+ 4 - 1
.vscode/settings.json

@@ -9,6 +9,9 @@
         "infrared_remote.h": "c",
         "xremote_navigation_view.h": "c",
         "xremote_settings_view.h": "c",
-        "xremote_learn_view.h": "c"
+        "xremote_learn_view.h": "c",
+        "system_error": "c",
+        "typeinfo": "c",
+        "xremote_analyzer.h": "c"
     }
 }

+ 14 - 3
README.md

@@ -40,8 +40,8 @@ Button name | Description
 ## Progress
 
 - [x] Application menu
-- [ ] Learn new remote
-- [ ] Signal analyzer
+- [x] Learn new remote
+- [x] Signal analyzer
 - [x] Use saved remote
     - [x] General button page
     - [x] Control buttons page
@@ -49,7 +49,7 @@ Button name | Description
     - [x] Player buttons page
     - [ ] Custom buttons page
     - [ ] Full button list
-    - [ ] Edit remote file
+    - [ ] Rename remote file
     - [ ] Delete remote file
 - [x] Application settings
     - [x] GUI to change settings
@@ -81,6 +81,17 @@ Button name | Description
     </tr>
 </table>
 
+<table align="center">
+    <tr>
+        <td align="center">Learn mode</td>
+        <td align="center">Received signal</td>
+    </tr>
+    <tr>
+        <td><img src="https://github.com/kala13x/flipper-xremote/blob/main/screens/learn_mode.png" alt="XRemote learn mode"></td>
+        <td><img src="https://github.com/kala13x/flipper-xremote/blob/main/screens/signal_view.png" alt="XRemote received signal"></td>
+    </tr>
+</table>
+
 <table align="center">
     <tr>
         <td align="center">Settings</td>

+ 15 - 0
infrared/infrared_remote.c

@@ -6,6 +6,8 @@
 
    Modifications made:
    - Added function infrared_remote_get_button_by_name()
+   - Added function infrared_remote_delete_button_by_name()
+   - Added function infrared_remote_push_button()
 */
 
 #include "infrared_remote.h"
@@ -113,6 +115,13 @@ bool infrared_remote_add_button(InfraredRemote* remote, const char* name, Infrar
     return infrared_remote_store(remote);
 }
 
+void infrared_remote_push_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);
+}
+
 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);
@@ -128,6 +137,12 @@ bool infrared_remote_delete_button(InfraredRemote* remote, size_t index) {
     return infrared_remote_store(remote);
 }
 
+bool infrared_remote_delete_button_by_name(InfraredRemote* remote, const char* name) {
+    size_t index = 0;
+    if (!infrared_remote_find_button_by_name(remote, name, &index)) return false;
+    return infrared_remote_delete_button(remote, index);
+}
+
 void infrared_remote_move_button(InfraredRemote* remote, size_t index_orig, size_t index_dest) {
     furi_assert(index_orig < InfraredButtonArray_size(remote->buttons));
     furi_assert(index_dest < InfraredButtonArray_size(remote->buttons));

+ 4 - 0
infrared/infrared_remote.h

@@ -6,6 +6,8 @@
 
    Modifications made:
    - Added function infrared_remote_get_button_by_name()
+   - Added function infrared_remote_delete_button_by_name()
+   - Added function infrared_remote_push_button()
 */
 
 #pragma once
@@ -32,8 +34,10 @@ bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* nam
 InfraredRemoteButton* infrared_remote_get_button_by_name(InfraredRemote* remote, const char* name);
 
 bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal);
+void infrared_remote_push_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_delete_button_by_name(InfraredRemote* remote, const char* name);
 void infrared_remote_move_button(InfraredRemote* remote, size_t index_orig, size_t index_dest);
 
 bool infrared_remote_store(InfraredRemote* remote);

BIN
screens/app_menu.png


BIN
screens/learn_mode.png


BIN
screens/saved_remote_apps.png


BIN
screens/signal_view.png


+ 50 - 10
views/xremote_common_view.c

@@ -9,8 +9,50 @@
 #include "xremote_common_view.h"
 #include "../xremote_app.h"
 
+typedef struct {
+    int index;
+    const char *name;
+} XRemoteButton;
+
+static const XRemoteButton g_buttons[XREMOTE_BUTTON_COUNT + 1] =
+{
+    { 0, XREMOTE_COMMAND_POWER },
+    { 1, XREMOTE_COMMAND_SETUP },
+    { 2, XREMOTE_COMMAND_INPUT },
+    { 3, XREMOTE_COMMAND_MENU },
+    { 4, XREMOTE_COMMAND_LIST },
+    { 5, XREMOTE_COMMAND_INFO },
+    { 6, XREMOTE_COMMAND_BACK },
+    { 7, XREMOTE_COMMAND_OK },
+    { 8, XREMOTE_COMMAND_UP },
+    { 9, XREMOTE_COMMAND_DOWN },
+    { 10, XREMOTE_COMMAND_LEFT },
+    { 11, XREMOTE_COMMAND_RIGHT },
+    { 12, XREMOTE_COMMAND_JUMP_FORWARD },
+    { 13, XREMOTE_COMMAND_JUMP_BACKWARD },
+    { 14, XREMOTE_COMMAND_FAST_FORWARD },
+    { 15, XREMOTE_COMMAND_FAST_BACKWARD },
+    { 16, XREMOTE_COMMAND_PLAY_PAUSE },
+    { 17, XREMOTE_COMMAND_PAUSE },
+    { 18, XREMOTE_COMMAND_PLAY },
+    { 19, XREMOTE_COMMAND_STOP },
+    { 20, XREMOTE_COMMAND_MUTE },
+    { 21, XREMOTE_COMMAND_MODE },
+    { 22, XREMOTE_COMMAND_VOL_UP },
+    { 23, XREMOTE_COMMAND_VOL_DOWN },
+    { 24, XREMOTE_COMMAND_NEXT_CHAN },
+    { 25, XREMOTE_COMMAND_PREV_CHAN },
+    { -1, NULL }
+};
+
+const char* xremote_button_get_name(int index)
+{
+    if (index > XREMOTE_BUTTON_COUNT) return NULL;
+    return g_buttons[index].name;
+}
+
 struct XRemoteView {
-    XRemoteViewClearCallback on_clear;
+    XRemoteClearCallback on_clear;
     XRemoteAppContext* app_ctx;
     View* view;
     void *context;
@@ -39,17 +81,14 @@ void xremote_view_clear_context(XRemoteView* rview)
 {
     furi_assert(rview);
 
-    if (rview->context != NULL &&
-        rview->on_clear != NULL)
-    {
+    if (rview->context && rview->on_clear)
         rview->on_clear(rview->context);
-        rview->context = NULL;
-    }
+
+    rview->context = NULL;
 }
 
-void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteViewClearCallback on_clear)
+void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteClearCallback on_clear)
 {
-    furi_assert(rview);
     xremote_view_clear_context(rview);
     rview->context = context;
     rview->on_clear = on_clear;
@@ -209,9 +248,10 @@ void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const ch
 
     canvas_set_font(canvas, FontPrimary);
     elements_multiline_text_aligned(canvas, x, 0, align, AlignTop, "XRemote");
-
     canvas_set_font(canvas, FontSecondary);
-    elements_multiline_text_aligned(canvas, x, 12, align, AlignTop, section);
+
+    if (section != NULL)
+        elements_multiline_text_aligned(canvas, x, 12, align, AlignTop, section);
 }
 
 void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char *text)

+ 22 - 2
views/xremote_common_view.h

@@ -20,6 +20,7 @@
 
 #include "../infrared/infrared_remote.h"
 
+#define XREMOTE_BUTTON_COUNT            26
 #define XREMOTE_COMMAND_POWER           "Power"
 #define XREMOTE_COMMAND_SETUP           "Setup"
 #define XREMOTE_COMMAND_INPUT           "Input"
@@ -47,6 +48,18 @@
 #define XREMOTE_COMMAND_NEXT_CHAN       "Ch_next"
 #define XREMOTE_COMMAND_PREV_CHAN       "Ch_prev"
 
+typedef enum {
+    XRemoteEventReserved = 200,
+    XRemoteEventSignalReceived,
+    XRemoteEventSignalFinish,
+    XRemoteEventSignalSave,
+    XRemoteEventSignalRetry,
+    XRemoteEventSignalSend,
+    XRemoteEventSignalSkip,
+    XRemoteEventSignalAskExit,
+    XRemoteEventSignalExit
+} XRemoteEvent;
+
 typedef enum {
     /* Navigation */
     XRemoteIconOk,
@@ -80,11 +93,15 @@ typedef struct {
 
 typedef enum {
     XRemoteViewNone,
+    XRemoteViewSignal,
+    XRemoteViewTextInput,
+    XRemoteViewDialogExit,
 
     /* Main page */
     XRemoteViewSubmenu,
     XRemoteViewLearn,
     XRemoteViewSaved,
+    XRemoteViewAnalyzer,
     XRemoteViewSettings,
     XRemoteViewAbout,
 
@@ -98,8 +115,11 @@ typedef enum {
 } XRemoteViewID;
 
 typedef struct XRemoteView XRemoteView;
-typedef void (*XRemoteViewClearCallback)(void *context);
+typedef void (*XRemoteClearCallback)(void *context);
 typedef void (*XRemoteViewDrawFunction)(Canvas*, XRemoteViewModel*);
+typedef XRemoteView* (*XRemoteViewAllocator)(void* app_ctx);
+
+const char* xremote_button_get_name(int index);
 
 void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const char* section);
 void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char *text);
@@ -118,7 +138,7 @@ InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView *rview, const
 bool xremote_view_press_button(XRemoteView *rview, InfraredRemoteButton* button);
 bool xremote_view_send_ir_msg_by_name(XRemoteView *rview, const char *name);
 
-void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteViewClearCallback on_clear);
+void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteClearCallback on_clear);
 void* xremote_view_get_context(XRemoteView* rview);
 void xremote_view_clear_context(XRemoteView* rview);
 void* xremote_view_get_app_context(XRemoteView* rview);

+ 219 - 9
views/xremote_learn_view.c

@@ -7,32 +7,242 @@
  */
 
 #include "xremote_learn_view.h"
+#include "../xremote_learn.h"
 #include "../xremote_app.h"
 
 static void xremote_learn_view_draw_callback(Canvas* canvas, void* context)
 {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteLearnContext* learn_ctx = model->context;
 
-    ViewOrientation orientation = app_ctx->app_settings->orientation;
-    uint64_t x = orientation == ViewOrientationVertical ? 70 : 34;
+    XRemoteAppContext* app_ctx = xremote_learn_get_app_context(learn_ctx);
+    const char *button_name = xremote_learn_get_curr_button_name(learn_ctx);
 
+    ViewOrientation orientation = app_ctx->app_settings->orientation;
     xremote_canvas_draw_header(canvas, orientation, "Learn");
-    canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 0, x, "Coming Soon.");
-    xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit");
+
+    char info_text[128];
+    snprintf(info_text, sizeof(info_text),
+        "Press\n\"%s\"\nbutton on\nthe remote.",
+        button_name != NULL ? button_name : "");
+
+    if (orientation == ViewOrientationHorizontal)
+    {
+        elements_multiline_text_aligned(canvas, 0, 12, AlignLeft, AlignTop, info_text);
+        xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 68, 22, "Finish", XRemoteIconEnter);
+        xremote_canvas_draw_button_wide(canvas, model->right_pressed, 68, 40, "Skip", XRemoteIconArrowRight);
+    }
+    else
+    {
+        elements_multiline_text_aligned(canvas, 0, 30, AlignLeft, AlignTop, info_text);
+        xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 82, "Finish", XRemoteIconEnter);
+        xremote_canvas_draw_button_wide(canvas, model->right_pressed, 0, 100, "Skip", XRemoteIconArrowRight);
+    }
+
+    const char *exit_str = xremote_app_context_get_exit_str(app_ctx);
+    xremote_canvas_draw_exit_footer(canvas, orientation, exit_str);
+}
+
+static void xremote_learn_success_view_draw_callback(Canvas* canvas, void* context)
+{
+    furi_assert(context);
+    XRemoteViewModel* model = context;
+    XRemoteLearnContext* learn_ctx = model->context;
+
+    XRemoteAppContext* app_ctx = xremote_learn_get_app_context(learn_ctx);
+    InfraredSignal *ir_signal = xremote_learn_get_ir_signal(learn_ctx);
+
+    xremote_canvas_draw_header(canvas, app_ctx->app_settings->orientation, NULL);
+    const char *button_name = xremote_learn_get_curr_button_name(learn_ctx);
+    char signal_info[128];
+
+    if (infrared_signal_is_raw(ir_signal))
+    {
+        InfraredRawSignal* raw = infrared_signal_get_raw_signal(ir_signal);
+
+        snprintf(signal_info, sizeof(signal_info),
+            "Name: %s\n"
+            "Type: RAW\n"
+            "T-Size: %u\n"
+            "D-Cycle: %.2f\n",
+            button_name,
+            raw->timings_size,
+            (double)raw->duty_cycle);
+    }
+    else
+    {
+        InfraredMessage* message = infrared_signal_get_message(ir_signal);
+        const char *infrared_protocol = infrared_get_protocol_name(message->protocol);
+
+        snprintf(signal_info, sizeof(signal_info),
+            "Name: %s\n"
+            "Proto: %s\n"
+            "Addr: 0x%lX\n"
+            "Cmd: 0x%lX\n",
+            button_name,
+            infrared_protocol,
+            message->address,
+            message->command);
+    }
+
+    if (app_ctx->app_settings->orientation == ViewOrientationHorizontal)
+    {
+        canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, "Received signal");
+        elements_multiline_text_aligned(canvas, 0, 16, AlignLeft, AlignTop, signal_info);
+        xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 68, 12, "Finish", XRemoteIconEnter);
+        xremote_canvas_draw_button_wide(canvas, model->right_pressed, 68, 30, "Next", XRemoteIconArrowRight);
+        xremote_canvas_draw_button_wide(canvas, model->back_pressed, 68, 48, "Retry", XRemoteIconBack);
+    }
+    else
+    {
+        canvas_draw_str_aligned(canvas, 0, 12, AlignLeft, AlignTop, "Received signal");
+        elements_multiline_text_aligned(canvas, 0, 27, AlignLeft, AlignTop, signal_info);
+        xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 76, "Finish", XRemoteIconEnter);
+        xremote_canvas_draw_button_wide(canvas, model->right_pressed, 0, 94, "Next", XRemoteIconArrowRight);
+        xremote_canvas_draw_button_wide(canvas, model->back_pressed, 0, 112, "Retry", XRemoteIconBack);
+    }
+}
+
+static void xremote_learn_success_view_process(XRemoteView* view, InputEvent* event)
+{
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            XRemoteLearnContext *learn_ctx = xremote_view_get_context(view);
+            model->context = learn_ctx;
+
+            if (event->type == InputTypePress)
+            {
+                if (event->key == InputKeyOk)
+                {
+                    model->ok_pressed = true;
+                    xremote_learn_send_event(learn_ctx, XRemoteEventSignalFinish);
+                }
+                else if (event->key == InputKeyBack)
+                {
+                    model->back_pressed = true;
+                    xremote_learn_send_event(learn_ctx, XRemoteEventSignalRetry);
+                }
+                else if (event->key == InputKeyRight)
+                {
+                    model->right_pressed = true;
+                    xremote_learn_send_event(learn_ctx, XRemoteEventSignalSave);
+                }
+            }
+            else if (event->type == InputTypeRelease)
+            {
+                if (event->key == InputKeyOk) model->ok_pressed = false;
+                else if (event->key == InputKeyBack) model->back_pressed = false;
+                else if (event->key == InputKeyRight) model->right_pressed = false;
+            }
+        },
+        true
+    );
+}
+
+static void xremote_learn_view_process(XRemoteView* view, InputEvent* event)
+{
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            XRemoteLearnContext *learn_ctx = xremote_view_get_context(view);
+            XRemoteAppContext *app_ctx = xremote_view_get_app_context(view);
+
+            XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
+            model->context = learn_ctx;
+
+            if (event->type == InputTypePress)
+            {
+                if (event->key == InputKeyOk && xremote_learn_has_buttons(learn_ctx))
+                {
+                    model->ok_pressed = true;
+                    xremote_learn_send_event(learn_ctx, XRemoteEventSignalFinish);
+                }
+                else if (event->key == InputKeyRight)
+                {
+                    model->right_pressed = true;
+                    xremote_learn_send_event(learn_ctx, XRemoteEventSignalSkip);
+                }
+            }
+            else if ((event->type == InputTypeShort ||
+                    event->type == InputTypeLong) &&
+                    event->key == InputKeyBack)
+            {
+                if ((event->type == InputTypeShort && exit == XRemoteAppExitPress) ||
+                    (event->type == InputTypeLong && exit == XRemoteAppExitHold))
+                {
+                    model->back_pressed = true;
+                    xremote_learn_send_event(learn_ctx, XRemoteEventSignalAskExit);
+                }
+            }
+            else if (event->type == InputTypeRelease)
+            {
+                if (event->key == InputKeyOk) model->ok_pressed = false;
+                else if (event->key == InputKeyBack) model->back_pressed = false;
+                else if (event->key == InputKeyRight) model->right_pressed = false;
+            }
+        },
+        true
+    );
+}
+
+static bool xremote_learn_success_view_input_callback(InputEvent* event, void* context)
+{
+    furi_assert(context);
+    XRemoteView* view = (XRemoteView*)context;
+    xremote_learn_success_view_process(view, event);
+    return true;
+}
+
+static bool xremote_learn_view_input_callback(InputEvent* event, void* context)
+{
+    furi_assert(context);
+    XRemoteView* view = (XRemoteView*)context;
+    xremote_learn_view_process(view, event);
+    return true;
+}
+
+XRemoteView* xremote_learn_success_view_alloc(void* app_ctx, void *learn_ctx)
+{
+    XRemoteView *view = xremote_view_alloc(app_ctx,
+        xremote_learn_success_view_input_callback,
+        xremote_learn_success_view_draw_callback);
+    xremote_view_set_context(view, learn_ctx, NULL);
+
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            model->context = learn_ctx;
+            model->right_pressed = false;
+            model->back_pressed = false;
+            model->ok_pressed = false;
+        },
+        true
+    );
+
+    return view;
 }
 
-XRemoteView* xremote_learn_view_alloc(void* app_ctx)
+XRemoteView* xremote_learn_view_alloc(void* app_ctx, void *learn_ctx)
 {
     XRemoteView *view = xremote_view_alloc(app_ctx,
-        NULL, xremote_learn_view_draw_callback);
+        xremote_learn_view_input_callback,
+        xremote_learn_view_draw_callback);
+    xremote_view_set_context(view, learn_ctx, NULL);
 
     with_view_model(
         xremote_view_get_view(view),
         XRemoteViewModel* model,
-        { model->context = app_ctx; },
+        {
+            model->context = learn_ctx;
+            model->right_pressed = false;
+            model->back_pressed = false;
+            model->ok_pressed = false;
+        },
         true
     );
 

+ 2 - 1
views/xremote_learn_view.h

@@ -10,4 +10,5 @@
 
 #include "xremote_common_view.h"
 
-XRemoteView* xremote_learn_view_alloc(void* app_ctx);
+XRemoteView* xremote_learn_view_alloc(void* app_ctx, void *learn_ctx);
+XRemoteView* xremote_learn_success_view_alloc(void* app_ctx, void *rx_ctx);

+ 7 - 7
views/xremote_player_view.c

@@ -17,8 +17,8 @@ static void xremote_player_view_draw_vertical(Canvas* canvas, XRemoteViewModel*
     xremote_canvas_draw_button(canvas, model->down_pressed, 23, 72, XRemoteIconJumpBackward);
     xremote_canvas_draw_button(canvas, model->left_pressed, 2, 51, XRemoteIconFastBackward);
     xremote_canvas_draw_button(canvas, model->right_pressed, 44, 51, XRemoteIconFastForward);
-    xremote_canvas_draw_button(canvas, model->back_pressed, 2, 95, XRemoteIconPause);
-    xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 51, XRemoteIconPlay);
+    xremote_canvas_draw_button(canvas, model->back_pressed, 2, 95, XRemoteIconPlay);
+    xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 51, XRemoteIconPause);
 
     if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 22, 107, &I_Hold_Text_17x4);
@@ -32,8 +32,8 @@ static void xremote_player_view_draw_horizontal(Canvas* canvas, XRemoteViewModel
     xremote_canvas_draw_button(canvas, model->down_pressed, 23, 44, XRemoteIconJumpBackward);
     xremote_canvas_draw_button(canvas, model->left_pressed, 2, 23, XRemoteIconFastBackward);
     xremote_canvas_draw_button(canvas, model->right_pressed, 44, 23, XRemoteIconFastForward);
-    xremote_canvas_draw_button(canvas, model->back_pressed, 70, 33, XRemoteIconPause);
-    xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 23, XRemoteIconPlay);
+    xremote_canvas_draw_button(canvas, model->back_pressed, 70, 33, XRemoteIconPlay);
+    xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 23, XRemoteIconPause);
 
     if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 90, 45, &I_Hold_Text_17x4);
@@ -91,7 +91,7 @@ static void xremote_player_view_process(XRemoteView* view, InputEvent* event)
                 }
                 else if (event->key == InputKeyOk)
                 {
-                    button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY);
+                    button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE);
                     if (xremote_view_press_button(view, button)) model->ok_pressed = true;
                 }
             }
@@ -99,14 +99,14 @@ static void xremote_player_view_process(XRemoteView* view, InputEvent* event)
                     event->key == InputKeyBack &&
                     exit == XRemoteAppExitHold)
             {
-                button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE);
+                button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY);
                 if (xremote_view_press_button(view, button)) model->back_pressed = true;
             }
             else if (event->type == InputTypeLong &&
                     event->key == InputKeyBack &&
                     exit == XRemoteAppExitPress)
             {
-                button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE);
+                button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY);
                 if (xremote_view_press_button(view, button)) model->back_pressed = true;
             }
             else if (event->type == InputTypeRelease)

+ 203 - 0
views/xremote_signal_view.c

@@ -0,0 +1,203 @@
+/*!
+ *  @file flipper-xremote/views/xremote_signal_view.c
+    @license This project is released under the GNU GPLv3 License
+ *  @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
+ *
+ * @brief Signal analyzer page view components and functionality.
+ */
+
+#include "xremote_signal_view.h"
+#include "../xremote_analyzer.h"
+#include "../xremote_app.h"
+
+static void xremote_signal_view_draw_callback(Canvas* canvas, void* context)
+{
+    furi_assert(context);
+    XRemoteViewModel* model = context;
+    XRemoteSignalAnalyzer* analyzer = model->context;
+    XRemoteAppContext* app_ctx = xremote_signal_analyzer_get_app_context(analyzer);
+
+    ViewOrientation orientation = app_ctx->app_settings->orientation;
+    uint8_t y = orientation == ViewOrientationHorizontal ? 17 : 49;
+    const char *text = "Press any\nbutton on\nthe remote.";
+
+    xremote_canvas_draw_header(canvas, orientation, "Analyzer");
+    elements_multiline_text_aligned(canvas, 0, y, AlignLeft, AlignTop, text);
+
+    const char *exit_str = xremote_app_context_get_exit_str(app_ctx);
+    xremote_canvas_draw_exit_footer(canvas, orientation, exit_str);
+}
+
+static void xremote_signal_success_view_draw_callback(Canvas* canvas, void* context)
+{
+    furi_assert(context);
+    XRemoteViewModel* model = context;
+    XRemoteSignalAnalyzer* analyzer = model->context;
+
+    XRemoteAppContext* app_ctx = xremote_signal_analyzer_get_app_context(analyzer);
+    InfraredSignal *ir_signal = xremote_signal_analyzer_get_ir_signal(analyzer);
+
+    xremote_canvas_draw_header(canvas, app_ctx->app_settings->orientation, "IR Signal");
+    char signal_info[128];
+
+    if (infrared_signal_is_raw(ir_signal))
+    {
+        InfraredRawSignal* raw = infrared_signal_get_raw_signal(ir_signal);
+
+        snprintf(signal_info, sizeof(signal_info),
+            "Type: RAW\n"
+            "T-Size: %u\n"
+            "D-Cycle: %.2f\n",
+            raw->timings_size,
+            (double)raw->duty_cycle);
+    }
+    else
+    {
+        InfraredMessage* message = infrared_signal_get_message(ir_signal);
+        const char *infrared_protocol = infrared_get_protocol_name(message->protocol);
+
+        snprintf(signal_info, sizeof(signal_info),
+            "Proto: %s\n"
+            "Addr: 0x%lX\n"
+            "Cmd: 0x%lX\n",
+            infrared_protocol,
+            message->address,
+            message->command);
+    }
+
+    if (app_ctx->app_settings->orientation == ViewOrientationHorizontal)
+    {
+        elements_multiline_text_aligned(canvas, 0, 17, AlignLeft, AlignTop, signal_info);
+        xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 68, 26, "Send", XRemoteIconEnter);
+        xremote_canvas_draw_button_wide(canvas, model->back_pressed, 68, 44, "Retry", XRemoteIconBack);
+    }
+    else
+    {
+        elements_multiline_text_aligned(canvas, 0, 39, AlignLeft, AlignTop, signal_info);
+        xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 88, "Send", XRemoteIconEnter);
+        xremote_canvas_draw_button_wide(canvas, model->back_pressed, 0, 106, "Retry", XRemoteIconBack);
+    }
+}
+
+static void xremote_signal_success_view_process(XRemoteView* view, InputEvent* event)
+{
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            XRemoteSignalAnalyzer *analyzer = xremote_view_get_context(view);
+            model->context = analyzer;
+
+            if (event->type == InputTypePress)
+            {
+                if (event->key == InputKeyOk)
+                {
+                    model->ok_pressed = true;
+                    xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalSend);
+                }
+                else if (event->key == InputKeyBack)
+                {
+                    model->back_pressed = true;
+                    xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalRetry);
+                }
+            }
+            else if (event->type == InputTypeRelease)
+            {
+                if (event->key == InputKeyOk) model->ok_pressed = false;
+                else if (event->key == InputKeyBack) model->back_pressed = false;
+            }
+        },
+        true
+    );
+}
+
+static void xremote_signal_view_process(XRemoteView* view, InputEvent* event)
+{
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            XRemoteSignalAnalyzer *analyzer = xremote_view_get_context(view);
+            XRemoteAppContext *app_ctx = xremote_view_get_app_context(view);
+
+            XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
+            model->context = analyzer;
+
+            if ((event->type == InputTypeShort ||
+                event->type == InputTypeLong) &&
+                event->key == InputKeyBack)
+            {
+                if ((event->type == InputTypeShort && exit == XRemoteAppExitPress) ||
+                    (event->type == InputTypeLong && exit == XRemoteAppExitHold))
+                {
+                    model->back_pressed = true;
+                    xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalExit);
+                }
+            }
+            else if (event->type == InputTypeRelease)
+            {
+                if (event->key == InputKeyOk) model->ok_pressed = false;
+                else if (event->key == InputKeyBack) model->back_pressed = false;
+                else if (event->key == InputKeyRight) model->right_pressed = false;
+            }
+        },
+        true
+    );
+}
+
+static bool xremote_signal_success_view_input_callback(InputEvent* event, void* context)
+{
+    furi_assert(context);
+    XRemoteView* view = (XRemoteView*)context;
+    xremote_signal_success_view_process(view, event);
+    return true;
+}
+
+static bool xremote_signal_view_input_callback(InputEvent* event, void* context)
+{
+    furi_assert(context);
+    XRemoteView* view = (XRemoteView*)context;
+    xremote_signal_view_process(view, event);
+    return true;
+}
+
+XRemoteView* xremote_signal_success_view_alloc(void* app_ctx, void *analyzer)
+{
+    XRemoteView *view = xremote_view_alloc(app_ctx,
+        xremote_signal_success_view_input_callback,
+        xremote_signal_success_view_draw_callback);
+    xremote_view_set_context(view, analyzer, NULL);
+
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            model->context = analyzer;
+            model->back_pressed = false;
+            model->ok_pressed = false;
+        },
+        true
+    );
+
+    return view;
+}
+
+XRemoteView* xremote_signal_view_alloc(void* app_ctx, void *analyzer)
+{
+    XRemoteView *view = xremote_view_alloc(app_ctx,
+        xremote_signal_view_input_callback,
+        xremote_signal_view_draw_callback);
+    xremote_view_set_context(view, analyzer, NULL);
+
+    with_view_model(
+        xremote_view_get_view(view),
+        XRemoteViewModel* model,
+        {
+            model->context = analyzer;
+            model->back_pressed = false;
+        },
+        true
+    );
+
+    return view;
+}

+ 14 - 0
views/xremote_signal_view.h

@@ -0,0 +1,14 @@
+/*!
+ *  @file flipper-xremote/views/xremote_signal_view.h
+    @license This project is released under the GNU GPLv3 License
+ *  @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
+ *
+ * @brief Signal analyzer page view components and functionality.
+ */
+
+#pragma once
+
+#include "xremote_common_view.h"
+
+XRemoteView* xremote_signal_view_alloc(void* app_ctx, void *learn_ctx);
+XRemoteView* xremote_signal_success_view_alloc(void* app_ctx, void *rx_ctx);

+ 5 - 0
xremote.c

@@ -10,9 +10,11 @@
 #include "xremote_learn.h"
 #include "xremote_control.h"
 #include "xremote_settings.h"
+#include "xremote_analyzer.h"
 
 #include "views/xremote_about_view.h"
 #include "views/xremote_learn_view.h"
+#include "views/xremote_signal_view.h"
 
 #define TAG "XRemote"
 
@@ -64,6 +66,8 @@ void xremote_submenu_callback(void* context, uint32_t index)
         child = xremote_learn_alloc(app->app_ctx);
     else if (index == XRemoteViewIRSubmenu)
         child = xremote_control_alloc(app->app_ctx);
+    else if (index == XRemoteViewAnalyzer)
+        child = xremote_analyzer_alloc(app->app_ctx);
     else if (index == XRemoteViewSettings)
         child = xremote_settings_alloc(app->app_ctx);
     else if (index == XRemoteViewAbout)
@@ -87,6 +91,7 @@ int32_t xremote_main(void* p)
     xremote_app_submenu_alloc(app, XRemoteViewSubmenu, xremote_exit_callback);
     xremote_app_submenu_add(app, "Learn", XRemoteViewLearn, xremote_submenu_callback);
     xremote_app_submenu_add(app, "Saved", XRemoteViewIRSubmenu, xremote_submenu_callback);
+    xremote_app_submenu_add(app, "Analyzer", XRemoteViewAnalyzer, xremote_submenu_callback);
     xremote_app_submenu_add(app, "Settings", XRemoteViewSettings, xremote_submenu_callback);
     xremote_app_submenu_add(app, "About", XRemoteViewAbout, xremote_submenu_callback);
 

+ 3 - 3
xremote.h

@@ -8,8 +8,8 @@
 
 #include "xremote_app.h"
 
-#define XREMOTE_VERSION_MAJOR  0
-#define XREMOTE_VERSION_MINOR  9
-#define XREMOTE_BUILD_NUMBER   26
+#define XREMOTE_VERSION_MAJOR  1
+#define XREMOTE_VERSION_MINOR  0
+#define XREMOTE_BUILD_NUMBER   1
 
 void xremote_get_version(char *version, size_t length);

+ 177 - 0
xremote_analyzer.c

@@ -0,0 +1,177 @@
+/*!
+ *  @file flipper-xremote/xremote_analyzer.c
+    @license This project is released under the GNU GPLv3 License
+ *  @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
+ *
+ * @brief Infrared remote singnal analyzer and custom view events.
+ */
+
+#include "xremote_analyzer.h"
+#include "views/xremote_signal_view.h"
+
+#define XREMOTE_TEXT_MAX    128
+
+struct XRemoteSignalAnalyzer {
+    XRemoteSignalReceiver* ir_receiver;
+    XRemoteAppContext* app_ctx;
+    XRemoteView* signal_view;
+    InfraredSignal* ir_signal;
+    XRemoteClearCallback on_clear;
+    void* context;
+    bool pause;
+};
+
+InfraredSignal* xremote_signal_analyzer_get_ir_signal(XRemoteSignalAnalyzer *analyzer)
+{
+    xremote_app_assert(analyzer, NULL);
+    return analyzer->ir_signal;
+}
+
+XRemoteSignalReceiver* xremote_signal_analyzer_get_ir_receiver(XRemoteSignalAnalyzer *analyzer)
+{
+    xremote_app_assert(analyzer, NULL);
+    return analyzer->ir_receiver;
+}
+
+XRemoteAppContext* xremote_signal_analyzer_get_app_context(XRemoteSignalAnalyzer *analyzer)
+{
+    xremote_app_assert(analyzer, NULL);
+    return analyzer->app_ctx;
+}
+
+void xremote_signal_analyzer_send_event(XRemoteSignalAnalyzer* analyzer, XRemoteEvent event)
+{
+    xremote_app_assert_void(analyzer);
+    ViewDispatcher* view_disp = analyzer->app_ctx->view_dispatcher;
+    view_dispatcher_send_custom_event(view_disp, event);
+}
+
+static void xremote_signal_analyzer_switch_to_view(XRemoteSignalAnalyzer* analyzer, XRemoteViewID view_id)
+{
+    xremote_app_assert_void(analyzer);
+    ViewDispatcher* view_disp = analyzer->app_ctx->view_dispatcher;
+    view_dispatcher_switch_to_view(view_disp, view_id);
+}
+
+static void xremote_signal_analyzer_rx_stop(XRemoteSignalAnalyzer *analyzer)
+{
+    xremote_app_assert_void(analyzer);
+    analyzer->pause = true;
+    xremote_signal_receiver_stop(analyzer->ir_receiver);
+}
+
+static void xremote_signal_analyzer_rx_start(XRemoteSignalAnalyzer *analyzer)
+{
+    xremote_app_assert_void(analyzer);
+    analyzer->pause = false;
+    xremote_signal_receiver_start(analyzer->ir_receiver);
+}
+
+static uint32_t xremote_signal_analyzer_view_exit_callback(void* context)
+{
+    UNUSED(context);
+    return XRemoteViewAnalyzer;
+}
+
+static void xremote_signal_analyzer_signal_callback(void *context, InfraredSignal* signal)
+{
+    XRemoteSignalAnalyzer* analyzer = context;
+    xremote_app_assert_void(!analyzer->pause);
+    analyzer->pause = true;
+
+    infrared_signal_set_signal(analyzer->ir_signal, signal);
+    xremote_signal_analyzer_send_event(analyzer, XRemoteEventSignalReceived);
+}
+
+static bool xremote_signal_analyzer_custom_event_callback(void* context, uint32_t event)
+{
+    xremote_app_assert(context, false);
+    XRemoteSignalAnalyzer *analyzer = context;
+
+    if (event == XRemoteEventSignalExit)
+    {
+        xremote_signal_analyzer_rx_stop(analyzer);
+        xremote_signal_analyzer_switch_to_view(analyzer, XRemoteViewSubmenu);
+    }
+    else if (event == XRemoteEventSignalReceived)
+    {
+        xremote_signal_analyzer_rx_stop(analyzer);
+        xremote_signal_analyzer_switch_to_view(analyzer, XRemoteViewSignal);
+    }
+    else if (event == XRemoteEventSignalRetry)
+    {
+        xremote_signal_analyzer_rx_start(analyzer);
+        xremote_signal_analyzer_switch_to_view(analyzer, XRemoteViewAnalyzer);
+    }
+    else if (event == XRemoteEventSignalSend)
+    {
+        XRemoteAppContext* app_ctx = analyzer->app_ctx;
+        xremote_app_send_signal(app_ctx, analyzer->ir_signal);
+    }
+
+    return true;
+}
+
+static XRemoteSignalAnalyzer* xremote_signal_analyzer_alloc(XRemoteAppContext* app_ctx)
+{
+    XRemoteSignalAnalyzer *analyzer = malloc(sizeof(XRemoteSignalAnalyzer));
+    analyzer->ir_signal = infrared_signal_alloc();
+    analyzer->app_ctx = app_ctx;
+    analyzer->pause = false;
+
+    analyzer->signal_view = xremote_signal_success_view_alloc(app_ctx, analyzer);
+    View* view = xremote_view_get_view(analyzer->signal_view);
+    view_set_previous_callback(view, xremote_signal_analyzer_view_exit_callback);
+    view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewSignal, view);
+
+    view_dispatcher_set_custom_event_callback(app_ctx->view_dispatcher, xremote_signal_analyzer_custom_event_callback);
+    view_dispatcher_set_event_callback_context(app_ctx->view_dispatcher, analyzer);
+
+    analyzer->ir_receiver = xremote_signal_receiver_alloc(app_ctx);
+    xremote_signal_receiver_set_context(analyzer->ir_receiver, analyzer, NULL);
+    xremote_signal_receiver_set_rx_callback(analyzer->ir_receiver, xremote_signal_analyzer_signal_callback);
+
+    return analyzer;
+}
+
+static void xremote_signal_analyzer_free(XRemoteSignalAnalyzer* analyzer)
+{
+    xremote_app_assert_void(analyzer);
+    xremote_signal_receiver_stop(analyzer->ir_receiver);
+
+    ViewDispatcher* view_disp = analyzer->app_ctx->view_dispatcher;
+    view_dispatcher_set_custom_event_callback(view_disp, NULL);
+    view_dispatcher_set_event_callback_context(view_disp, NULL);
+
+    view_dispatcher_remove_view(view_disp, XRemoteViewSignal);
+    xremote_view_free(analyzer->signal_view);
+
+    xremote_signal_receiver_free(analyzer->ir_receiver);
+    infrared_signal_free(analyzer->ir_signal);
+    free(analyzer);
+}
+
+static void xremote_signal_analyzer_clear_callback(void* context)
+{
+    XRemoteSignalAnalyzer *analyzer = context;
+    xremote_signal_analyzer_free(analyzer);
+}
+
+XRemoteApp* xremote_analyzer_alloc(XRemoteAppContext* app_ctx)
+{
+    XRemoteApp* app = xremote_app_alloc(app_ctx);
+    app->view_id = XRemoteViewAnalyzer;
+
+    XRemoteSignalAnalyzer* analyzer = xremote_signal_analyzer_alloc(app_ctx);
+    app->view_ctx = xremote_signal_view_alloc(app->app_ctx, analyzer);
+    View* view = xremote_view_get_view(app->view_ctx);
+
+    ViewDispatcher* view_disp = app_ctx->view_dispatcher;
+    view_dispatcher_add_view(view_disp, app->view_id, view);
+
+    xremote_app_view_set_previous_callback(app, xremote_signal_analyzer_view_exit_callback);
+    xremote_app_set_view_context(app, analyzer, xremote_signal_analyzer_clear_callback);
+
+    xremote_signal_receiver_start(analyzer->ir_receiver);
+    return app;
+}

+ 21 - 0
xremote_analyzer.h

@@ -0,0 +1,21 @@
+/*!
+ *  @file flipper-xremote/xremote_analyzer.h
+    @license This project is released under the GNU GPLv3 License
+ *  @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
+ *
+ * @brief Infrared remote singnal analyzer and custom view events.
+ */
+
+#pragma once
+
+#include "xremote_app.h"
+#include "xremote_signal.h"
+
+typedef struct XRemoteSignalAnalyzer XRemoteSignalAnalyzer;
+
+void xremote_signal_analyzer_send_event(XRemoteSignalAnalyzer* analyzer, XRemoteEvent event);
+XRemoteSignalReceiver* xremote_signal_analyzer_get_ir_receiver(XRemoteSignalAnalyzer *analyzer);
+XRemoteAppContext* xremote_signal_analyzer_get_app_context(XRemoteSignalAnalyzer *analyzer);
+InfraredSignal* xremote_signal_analyzer_get_ir_signal(XRemoteSignalAnalyzer *analyzer);
+
+XRemoteApp* xremote_analyzer_alloc(XRemoteAppContext* app_ctx);

+ 19 - 4
xremote_app.c

@@ -115,6 +115,7 @@ XRemoteAppContext* xremote_app_context_alloc(void *arg)
     ctx->view_dispatcher = view_dispatcher_alloc();
     view_dispatcher_enable_queue(ctx->view_dispatcher);
     view_dispatcher_attach_to_gui(ctx->view_dispatcher, ctx->gui, ViewDispatcherTypeFullscreen);
+
     return ctx;
 }
 
@@ -144,11 +145,25 @@ const char* xremote_app_context_get_exit_str(XRemoteAppContext* ctx)
     return exit_behavior == XRemoteAppExitHold ? "Hold to exit" : "Press to exit";
 }
 
+void xremote_app_notification_blink(NotificationApp* notifications)
+{
+    xremote_app_assert_void(notifications);
+    notification_message(notifications, &g_sequence_blink_purple_50);
+}
+
 void xremote_app_context_notify_led(XRemoteAppContext* app_ctx)
 {
     xremote_app_assert_void(app_ctx);
-    NotificationApp* notifications = app_ctx->notifications;
-    notification_message(notifications, &g_sequence_blink_purple_50);
+    xremote_app_notification_blink(app_ctx->notifications);
+}
+
+bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal)
+{
+    xremote_app_assert(signal, false);
+    XRemoteAppSettings* settings = app_ctx->app_settings;
+    infrared_signal_transmit_times(signal, settings->repeat_count);
+    xremote_app_context_notify_led(app_ctx);
+    return true;
 }
 
 void xremote_app_view_alloc(XRemoteApp *app, uint32_t view_id, XRemoteViewAllocator allocator)
@@ -259,14 +274,14 @@ void xremote_app_view_set_previous_callback(XRemoteApp* app, ViewNavigationCallb
     view_set_previous_callback(view, callback);
 }
 
-void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteViewClearCallback on_clear)
+void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear)
 {
     furi_assert(app);
     xremote_app_assert_void(app->view_ctx);
     xremote_view_set_context(app->view_ctx, context, on_clear);
 }
 
-void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteAppClearCallback on_clear)
+void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear)
 {
     furi_assert(app);
     app->on_clear = on_clear;

+ 7 - 6
xremote_app.h

@@ -14,6 +14,7 @@
 #include <gui/view_dispatcher.h>
 #include <gui/modules/submenu.h>
 #include <gui/modules/dialog_ex.h>
+#include <gui/modules/text_input.h>
 #include <gui/modules/variable_item_list.h>
 
 #include <notification/notification.h>
@@ -22,6 +23,7 @@
 #include <flipper_format/flipper_format.h>
 #include <storage/storage.h>
 #include <dialogs/dialogs.h>
+#include <infrared_worker.h>
 
 #include "views/xremote_common_view.h"
 #include "xc_icons.h"
@@ -63,12 +65,11 @@ void xremote_app_context_free(XRemoteAppContext* ctx);
 
 const char* xremote_app_context_get_exit_str(XRemoteAppContext* ctx);
 void xremote_app_context_notify_led(XRemoteAppContext* app_ctx);
-
-typedef XRemoteView* (*XRemoteViewAllocator)(void* app_ctx);
-typedef void (*XRemoteAppClearCallback)(void *context);
+void xremote_app_notification_blink(NotificationApp* notifications);
+bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal);
 
 typedef struct {
-    XRemoteAppClearCallback on_clear;
+    XRemoteClearCallback on_clear;
     XRemoteAppContext* app_ctx;
     XRemoteViewID submenu_id;
     XRemoteViewID view_id;
@@ -85,8 +86,8 @@ void xremote_app_view_alloc(XRemoteApp *app, uint32_t view_id, XRemoteViewAlloca
 void xremote_app_view_free(XRemoteApp* app);
 
 void xremote_app_view_set_previous_callback(XRemoteApp* app, ViewNavigationCallback callback);
-void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteViewClearCallback on_clear);
-void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteAppClearCallback on_clear);
+void xremote_app_set_view_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear);
+void xremote_app_set_user_context(XRemoteApp* app, void *context, XRemoteClearCallback on_clear);
 void xremote_app_user_context_free(XRemoteApp* app);
 
 bool xremote_app_has_view(XRemoteApp *app, uint32_t view_id);

+ 422 - 2
xremote_learn.c

@@ -9,16 +9,436 @@
 #include "xremote_learn.h"
 #include "views/xremote_learn_view.h"
 
+#define XREMOTE_TEXT_MAX    128
+
+struct XRemoteLearnContext {
+    /* XRemote context */
+    XRemoteSignalReceiver* ir_receiver;
+    XRemoteAppContext* app_ctx;
+    XRemoteView* signal_view;
+    XRemoteViewID curr_view;
+    XRemoteViewID prev_view;
+
+    /* Main infrared app context */
+    InfraredRemote* ir_remote;
+    InfraredSignal* ir_signal;
+
+    /* User interactions */
+    TextInput* text_input;
+    DialogEx* dialog_ex;
+
+    /* User context and clear callback */
+    XRemoteClearCallback on_clear;
+    void* context;
+
+    /* Private control flags */
+    char text_store[XREMOTE_TEXT_MAX + 1];
+    uint8_t current_button;
+    bool finish_learning;
+    bool stop_receiver;
+    bool is_dirty;
+};
+
+void xremote_learn_send_event(XRemoteLearnContext* learn_ctx, XRemoteEvent event)
+{
+    xremote_app_assert_void(learn_ctx);
+    if (event == XRemoteEventSignalFinish) learn_ctx->finish_learning = true;
+    ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher;
+    view_dispatcher_send_custom_event(view_disp, event);
+}
+
+const char* xremote_learn_get_curr_button_name(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, NULL);
+    return xremote_button_get_name(learn_ctx->current_button);
+}
+
+int xremote_learn_get_curr_button_index(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, -1);
+    return learn_ctx->current_button;
+}
+
+InfraredRemote* xremote_learn_get_ir_remote(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, NULL);
+    return learn_ctx->ir_remote;
+}
+
+InfraredSignal* xremote_learn_get_ir_signal(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, NULL);
+    return learn_ctx->ir_signal;
+}
+
+XRemoteSignalReceiver* xremote_learn_get_ir_receiver(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, NULL);
+    return learn_ctx->ir_receiver;
+}
+
+XRemoteAppContext* xremote_learn_get_app_context(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, NULL);
+    return learn_ctx->app_ctx;
+}
+
+bool xremote_learn_has_buttons(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert(learn_ctx, false);
+    return infrared_remote_get_button_count(learn_ctx->ir_remote) > 0;
+}
+
+static void xremote_learn_switch_to_view(XRemoteLearnContext* learn_ctx, XRemoteViewID view_id)
+{
+    xremote_app_assert_void(learn_ctx);
+    learn_ctx->prev_view = learn_ctx->curr_view;
+    learn_ctx->curr_view = view_id;
+    ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher;
+    view_dispatcher_switch_to_view(view_disp, view_id);
+}
+
+static void xremote_learn_context_rx_stop(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert_void(learn_ctx);
+    learn_ctx->stop_receiver = true;
+    xremote_signal_receiver_stop(learn_ctx->ir_receiver);
+}
+
+static void xremote_learn_context_rx_start(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert_void(learn_ctx);
+    learn_ctx->finish_learning = false;
+    learn_ctx->stop_receiver = false;
+    xremote_signal_receiver_start(learn_ctx->ir_receiver);
+}
+
+
+static void xremote_learn_exit_dialog_free(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert_void(learn_ctx);
+    xremote_app_assert_void(learn_ctx->dialog_ex);
+
+    ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher;
+    view_dispatcher_remove_view(view_disp, XRemoteViewDialogExit);
+
+    dialog_ex_free(learn_ctx->dialog_ex);
+    learn_ctx->dialog_ex = NULL;
+}
+
+static void xremote_learn_exit_dialog_alloc(XRemoteLearnContext *learn_ctx, DialogExResultCallback callback)
+{
+    xremote_app_assert_void(learn_ctx);
+    xremote_learn_exit_dialog_free(learn_ctx);
+
+    ViewDispatcher *view_disp = learn_ctx->app_ctx->view_dispatcher;
+    const char* dialog_text = "All unsaved data\nwill be lost!";
+    const char* header_text = "Exit to XRemote Menu?";
+
+    learn_ctx->dialog_ex = dialog_ex_alloc();
+    View *view = dialog_ex_get_view(learn_ctx->dialog_ex);
+    view_dispatcher_add_view(view_disp, XRemoteViewDialogExit, view);
+
+    dialog_ex_set_header(learn_ctx->dialog_ex, header_text, 64, 11, AlignCenter, AlignTop);
+    dialog_ex_set_text(learn_ctx->dialog_ex, dialog_text, 64, 25, AlignCenter, AlignTop);
+    dialog_ex_set_icon(learn_ctx->dialog_ex, 0, 0, NULL);
+
+    dialog_ex_set_left_button_text(learn_ctx->dialog_ex, "Exit");
+    dialog_ex_set_center_button_text(learn_ctx->dialog_ex, "Save");
+    dialog_ex_set_right_button_text(learn_ctx->dialog_ex, "Stay");
+
+    dialog_ex_set_result_callback(learn_ctx->dialog_ex, callback);
+    dialog_ex_set_context(learn_ctx->dialog_ex, learn_ctx);
+}
+
 static uint32_t xremote_learn_view_exit_callback(void* context)
 {
     UNUSED(context);
-    return XRemoteViewSubmenu;
+    return XRemoteViewLearn;
+}
+
+static void xremote_learn_dialog_exit_callback(DialogExResult result, void* context)
+{
+    XRemoteLearnContext* learn_ctx = (XRemoteLearnContext*)context;
+    xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu);
+
+    if (result == DialogExResultLeft)
+        xremote_learn_send_event(learn_ctx, XRemoteEventSignalExit);
+    else if (result == DialogExResultRight)
+        xremote_learn_send_event(learn_ctx, XRemoteEventSignalRetry);
+    else if (result == DialogExResultCenter)
+        xremote_learn_send_event(learn_ctx, XRemoteEventSignalFinish);
+}
+
+static uint32_t xremote_learn_text_input_exit_callback(void* context)
+{
+    TextInput* text_input = context;
+    XRemoteLearnContext* learn_ctx;
+
+    learn_ctx = text_input_get_validator_callback_context(text_input);
+    xremote_app_assert(learn_ctx, XRemoteViewSubmenu);
+
+    XRemoteEvent event = learn_ctx->prev_view == XRemoteViewSignal ?
+            XRemoteEventSignalReceived : XRemoteEventSignalRetry;
+
+    if (learn_ctx->current_button >= XREMOTE_BUTTON_COUNT)
+        learn_ctx->current_button = XREMOTE_BUTTON_COUNT - 1;
+
+    learn_ctx->finish_learning = false;
+    xremote_learn_send_event(learn_ctx, event);
+
+    return XRemoteViewTextInput;
+}
+
+static void xremote_learn_text_input_callback(void* context)
+{
+    xremote_app_assert_void(context);
+    XRemoteLearnContext *learn_ctx = context;
+
+    if (learn_ctx->is_dirty)
+    {
+        const char* name = xremote_learn_get_curr_button_name(learn_ctx);
+        if (!infrared_remote_get_button_by_name(learn_ctx->ir_remote, name))
+        {
+            InfraredSignal* signal = xremote_learn_get_ir_signal(learn_ctx);
+            infrared_remote_push_button(learn_ctx->ir_remote, name, signal);
+        }
+
+        learn_ctx->is_dirty = false;
+    }
+
+    if (infrared_remote_get_button_count(learn_ctx->ir_remote) && learn_ctx->text_store[0] != '\0')
+    {
+        char output_file[256];
+        snprintf(output_file, sizeof(output_file), "%s/%s.ir",
+            XREMOTE_APP_FOLDER, learn_ctx->text_store);
+
+        infrared_remote_set_name(learn_ctx->ir_remote, learn_ctx->text_store);
+        infrared_remote_set_path(learn_ctx->ir_remote, output_file);
+        infrared_remote_store(learn_ctx->ir_remote);
+        infrared_remote_reset(learn_ctx->ir_remote);
+    }
+
+    xremote_learn_send_event(learn_ctx, XRemoteEventSignalExit);
+}
+
+static void xremote_learn_signal_callback(void *context, InfraredSignal* signal)
+{
+    XRemoteLearnContext* learn_ctx = context;
+    xremote_app_assert_void(!learn_ctx->stop_receiver);
+    xremote_app_assert_void(!learn_ctx->finish_learning);
+
+    learn_ctx->stop_receiver = true;
+    learn_ctx->is_dirty = true;
+
+    infrared_signal_set_signal(learn_ctx->ir_signal, signal);
+    xremote_learn_send_event(learn_ctx, XRemoteEventSignalReceived);
+}
+
+static void xremote_learn_exit_dialog_ask(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert_void(learn_ctx);
+
+    if (infrared_remote_get_button_count(learn_ctx->ir_remote) || learn_ctx->is_dirty)
+    {
+        xremote_learn_exit_dialog_alloc(learn_ctx, xremote_learn_dialog_exit_callback);
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewDialogExit);
+        return;
+    }
+
+    learn_ctx->is_dirty = false;
+    xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu);
+}
+
+static void xremote_learn_finish(XRemoteLearnContext *learn_ctx)
+{
+    xremote_app_assert_void(learn_ctx);
+    xremote_app_assert_void(learn_ctx->text_input);
+
+    if (infrared_remote_get_button_count(learn_ctx->ir_remote) || learn_ctx->is_dirty)
+    {
+        snprintf(learn_ctx->text_store, XREMOTE_TEXT_MAX, "Remote_");
+        text_input_set_header_text(learn_ctx->text_input, "Name new remote");
+
+        text_input_set_result_callback(
+            learn_ctx->text_input,
+            xremote_learn_text_input_callback,
+            learn_ctx,
+            learn_ctx->text_store,
+            XREMOTE_TEXT_MAX,
+            true);
+
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewTextInput);
+        return;
+    }
+
+    learn_ctx->is_dirty = false;
+    xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu);
+}
+
+static bool xremote_learn_custom_event_callback(void* context, uint32_t event)
+{
+    xremote_app_assert(context, false);
+    XRemoteLearnContext *learn_ctx = context;
+
+    if (learn_ctx->finish_learning &&
+        event != XRemoteEventSignalFinish &&
+        event != XRemoteEventSignalAskExit &&
+        event != XRemoteEventSignalExit) return true;
+
+    if (event == XRemoteEventSignalReceived)
+    {
+        xremote_learn_context_rx_stop(learn_ctx);
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewSignal);
+    }
+    else if (event == XRemoteEventSignalSave)
+    {
+        const char* name = xremote_learn_get_curr_button_name(learn_ctx);
+        infrared_remote_delete_button_by_name(learn_ctx->ir_remote, name);
+
+        InfraredSignal* signal = xremote_learn_get_ir_signal(learn_ctx);
+        infrared_remote_push_button(learn_ctx->ir_remote, name, signal);
+        learn_ctx->is_dirty = false;
+
+        if (++learn_ctx->current_button >= XREMOTE_BUTTON_COUNT)
+        {
+            xremote_learn_context_rx_stop(learn_ctx);
+            xremote_learn_finish(learn_ctx);
+            return true;
+        }
+
+        xremote_learn_context_rx_start(learn_ctx);
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewLearn);
+    }
+    else if (event == XRemoteEventSignalSkip)
+    {
+        learn_ctx->current_button++;
+        learn_ctx->is_dirty = false;
+
+        if (learn_ctx->current_button >= XREMOTE_BUTTON_COUNT)
+        {
+            if (xremote_learn_has_buttons(learn_ctx))
+            {
+                xremote_learn_context_rx_stop(learn_ctx);
+                xremote_learn_finish(learn_ctx);
+                return true;
+            }
+
+            learn_ctx->current_button = 0;
+        }
+
+        xremote_learn_context_rx_start(learn_ctx);
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewLearn);
+    }
+    else if (event == XRemoteEventSignalRetry)
+    {
+        learn_ctx->is_dirty = false;
+        xremote_learn_context_rx_start(learn_ctx);
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewLearn);
+    }
+    else if (event == XRemoteEventSignalFinish)
+    {
+        xremote_learn_context_rx_stop(learn_ctx);
+        xremote_learn_finish(learn_ctx);
+    }
+    else if (event == XRemoteEventSignalAskExit)
+    {
+        xremote_learn_context_rx_stop(learn_ctx);
+        xremote_learn_exit_dialog_ask(learn_ctx);
+    }
+    else if (event == XRemoteEventSignalExit)
+    {
+        learn_ctx->is_dirty = false;
+        xremote_learn_context_rx_stop(learn_ctx);
+        xremote_learn_switch_to_view(learn_ctx, XRemoteViewSubmenu);
+    }
+
+    return true;
+}
+
+static XRemoteLearnContext* xremote_learn_context_alloc(XRemoteAppContext* app_ctx)
+{
+    XRemoteLearnContext *learn_ctx = malloc(sizeof(XRemoteLearnContext));
+    learn_ctx->ir_signal = infrared_signal_alloc();
+    learn_ctx->ir_remote = infrared_remote_alloc();
+
+    learn_ctx->app_ctx = app_ctx;
+    learn_ctx->dialog_ex = NULL;
+    learn_ctx->text_store[0] = 0;
+    learn_ctx->current_button = 0;
+
+    learn_ctx->curr_view = XRemoteViewLearn;
+    learn_ctx->prev_view = XRemoteViewNone;
+
+    learn_ctx->finish_learning = false;
+    learn_ctx->stop_receiver = false;
+    learn_ctx->is_dirty = false;
+
+    learn_ctx->signal_view = xremote_learn_success_view_alloc(app_ctx, learn_ctx);
+    View* view = xremote_view_get_view(learn_ctx->signal_view);
+    view_set_previous_callback(view, xremote_learn_view_exit_callback);
+    view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewSignal, view);
+
+    learn_ctx->text_input = text_input_alloc();
+    text_input_set_validator(learn_ctx->text_input, NULL, learn_ctx);
+
+    view = text_input_get_view(learn_ctx->text_input);
+    view_set_previous_callback(view, xremote_learn_text_input_exit_callback);
+    view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewTextInput, view);
+
+    view_dispatcher_set_custom_event_callback(app_ctx->view_dispatcher, xremote_learn_custom_event_callback);
+    view_dispatcher_set_event_callback_context(app_ctx->view_dispatcher, learn_ctx);
+
+    learn_ctx->ir_receiver = xremote_signal_receiver_alloc(app_ctx);
+    xremote_signal_receiver_set_context(learn_ctx->ir_receiver, learn_ctx, NULL);
+    xremote_signal_receiver_set_rx_callback(learn_ctx->ir_receiver, xremote_learn_signal_callback);
+
+    return learn_ctx;
+}
+
+static void xremote_learn_context_free(XRemoteLearnContext* learn_ctx)
+{
+    xremote_app_assert_void(learn_ctx);
+    xremote_signal_receiver_stop(learn_ctx->ir_receiver);
+    xremote_learn_exit_dialog_free(learn_ctx);
+
+    ViewDispatcher* view_disp = learn_ctx->app_ctx->view_dispatcher;
+    view_dispatcher_set_custom_event_callback(view_disp, NULL);
+    view_dispatcher_set_event_callback_context(view_disp, NULL);
+
+    view_dispatcher_remove_view(view_disp, XRemoteViewTextInput);
+    text_input_free(learn_ctx->text_input);
+
+    view_dispatcher_remove_view(view_disp, XRemoteViewSignal);
+    xremote_view_free(learn_ctx->signal_view);
+
+    xremote_signal_receiver_free(learn_ctx->ir_receiver);
+    infrared_signal_free(learn_ctx->ir_signal);
+    infrared_remote_free(learn_ctx->ir_remote);
+    free(learn_ctx);
+}
+
+static void xremote_learn_context_clear_callback(void* context)
+{
+    XRemoteLearnContext *learn = context;
+    xremote_learn_context_free(learn);
 }
 
 XRemoteApp* xremote_learn_alloc(XRemoteAppContext* app_ctx)
 {
     XRemoteApp* app = xremote_app_alloc(app_ctx);
-    xremote_app_view_alloc(app, XRemoteViewLearn, xremote_learn_view_alloc);
+    app->view_id = XRemoteViewLearn;
+
+    XRemoteLearnContext* learn = xremote_learn_context_alloc(app_ctx);
+    app->view_ctx = xremote_learn_view_alloc(app->app_ctx, learn);
+    View* view = xremote_view_get_view(app->view_ctx);
+
+    ViewDispatcher* view_disp = app_ctx->view_dispatcher;
+    view_dispatcher_add_view(view_disp, app->view_id, view);
+
     xremote_app_view_set_previous_callback(app, xremote_learn_view_exit_callback);
+    xremote_app_set_view_context(app, learn, xremote_learn_context_clear_callback);
+
+    xremote_signal_receiver_start(learn->ir_receiver);
     return app;
 }

+ 13 - 0
xremote_learn.h

@@ -9,5 +9,18 @@
 #pragma once
 
 #include "xremote_app.h"
+#include "xremote_signal.h"
+
+typedef struct XRemoteLearnContext XRemoteLearnContext;
+
+void xremote_learn_send_event(XRemoteLearnContext* learn_ctx, XRemoteEvent event);
+const char* xremote_learn_get_curr_button_name(XRemoteLearnContext *learn_ctx);
+int xremote_learn_get_curr_button_index(XRemoteLearnContext *learn_ctx);
+bool xremote_learn_has_buttons(XRemoteLearnContext *learn_ctx);
+
+XRemoteSignalReceiver* xremote_learn_get_ir_receiver(XRemoteLearnContext *learn_ctx);
+XRemoteAppContext* xremote_learn_get_app_context(XRemoteLearnContext *learn_ctx);
+InfraredRemote* xremote_learn_get_ir_remote(XRemoteLearnContext *learn_ctx);
+InfraredSignal* xremote_learn_get_ir_signal(XRemoteLearnContext *learn_ctx);
 
 XRemoteApp* xremote_learn_alloc(XRemoteAppContext* app_ctx);

+ 131 - 0
xremote_signal.c

@@ -0,0 +1,131 @@
+/*!
+ *  @file flipper-xremote/xremote_signal.h
+    @license This project is released under the GNU GPLv3 License
+ *  @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
+ *
+ * @brief Implementation of infrared signal receiver functionality 
+ */
+
+#include "xremote_signal.h"
+
+struct XRemoteSignalReceiver {
+    XRemoteClearCallback on_clear;
+    XRemoteRxCallback rx_callback;
+
+    NotificationApp* notifications;
+    InfraredWorker* worker;
+    InfraredSignal* signal;
+
+    void* context;
+    bool started;
+};
+
+static void xremote_signal_receiver_rx_callback(void* context, InfraredWorkerSignal* ir_signal)
+{
+    furi_assert(context);
+    XRemoteSignalReceiver *rx_ctx = context;
+    xremote_app_notification_blink(rx_ctx->notifications);
+
+    if (infrared_worker_signal_is_decoded(ir_signal))
+    {
+        const InfraredMessage* message;
+        message = infrared_worker_get_decoded_signal(ir_signal);
+        infrared_signal_set_message(rx_ctx->signal, message);
+    }
+    else
+    {
+        const uint32_t* timings;
+        size_t timings_size = 0;
+
+        infrared_worker_get_raw_signal(ir_signal, &timings, &timings_size);
+        infrared_signal_set_raw_signal(rx_ctx->signal, timings, timings_size,
+            INFRARED_COMMON_CARRIER_FREQUENCY, INFRARED_COMMON_DUTY_CYCLE);
+    }
+
+    if (rx_ctx->rx_callback != NULL)
+        rx_ctx->rx_callback(rx_ctx->context, rx_ctx->signal);
+}
+
+static void xremote_signal_receiver_clear_context(XRemoteSignalReceiver* rx_ctx)
+{
+    xremote_app_assert_void(rx_ctx);
+    xremote_app_assert_void(rx_ctx->context);
+    xremote_app_assert_void(rx_ctx->on_clear);
+    rx_ctx->on_clear(rx_ctx->context);
+    rx_ctx->context = NULL;
+}
+
+XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx)
+{
+    XRemoteSignalReceiver *rx_ctx = malloc(sizeof(XRemoteSignalReceiver));
+    rx_ctx->signal = infrared_signal_alloc();
+    rx_ctx->worker = infrared_worker_alloc();
+
+    rx_ctx->notifications = app_ctx->notifications;
+    rx_ctx->rx_callback = NULL;
+    rx_ctx->on_clear = NULL;
+    rx_ctx->context = NULL;
+    rx_ctx->started = false;
+    return rx_ctx;
+}
+
+void xremote_signal_receiver_free(XRemoteSignalReceiver* rx_ctx)
+{
+    xremote_app_assert_void(rx_ctx);
+    xremote_signal_receiver_stop(rx_ctx);
+    infrared_worker_free(rx_ctx->worker);
+    infrared_signal_free(rx_ctx->signal);
+    xremote_signal_receiver_clear_context(rx_ctx);
+    free(rx_ctx);
+}
+
+void xremote_signal_receiver_set_context(XRemoteSignalReceiver* rx_ctx, void *context, XRemoteClearCallback on_clear)
+{
+    xremote_signal_receiver_clear_context(rx_ctx);
+    rx_ctx->on_clear = on_clear;
+    rx_ctx->context = context;
+}
+
+void xremote_signal_receiver_set_rx_callback(XRemoteSignalReceiver* rx_ctx, XRemoteRxCallback rx_callback)
+{
+    xremote_app_assert_void(rx_ctx);
+    rx_ctx->rx_callback = rx_callback;
+}
+
+void xremote_signal_receiver_start(XRemoteSignalReceiver *rx_ctx)
+{
+    xremote_app_assert_void((rx_ctx && rx_ctx->worker && !rx_ctx->started));
+    infrared_worker_rx_set_received_signal_callback(rx_ctx->worker,
+        xremote_signal_receiver_rx_callback, (void*)rx_ctx);
+
+    infrared_worker_rx_start(rx_ctx->worker);
+    xremote_app_notification_blink(rx_ctx->notifications);
+    rx_ctx->started = true;
+}
+
+void xremote_signal_receiver_stop(XRemoteSignalReceiver *rx_ctx)
+{
+    xremote_app_assert_void((rx_ctx && rx_ctx->worker && rx_ctx->started));
+    infrared_worker_rx_set_received_signal_callback(rx_ctx->worker, NULL, NULL);
+    infrared_worker_rx_stop(rx_ctx->worker);
+    rx_ctx->started = false;
+}
+
+void xremote_signal_receiver_pause(XRemoteSignalReceiver *rx_ctx)
+{
+    xremote_app_assert_void((rx_ctx && rx_ctx->worker));
+    infrared_worker_rx_set_received_signal_callback(rx_ctx->worker, NULL, NULL);
+}
+
+void xremote_signal_receiver_resume(XRemoteSignalReceiver *rx_ctx)
+{
+    xremote_app_assert_void((rx_ctx && rx_ctx->worker));
+    infrared_worker_rx_set_received_signal_callback(rx_ctx->worker,
+            xremote_signal_receiver_rx_callback, (void*)rx_ctx);
+}
+
+InfraredSignal* xremote_signal_receiver_get_signal(XRemoteSignalReceiver *rx_ctx)
+{
+    xremote_app_assert(rx_ctx, NULL);
+    return rx_ctx->signal;
+}

+ 28 - 0
xremote_signal.h

@@ -0,0 +1,28 @@
+/*!
+ *  @file flipper-xremote/xremote_signal.h
+    @license This project is released under the GNU GPLv3 License
+ *  @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
+ *
+ * @brief Implementation of infrared signal receiver functionality 
+ */
+
+#pragma once
+
+#include "xremote_app.h"
+#include "infrared/infrared_signal.h"
+
+typedef void (*XRemoteRxCallback)(void *context, InfraredSignal* signal);
+typedef struct XRemoteSignalReceiver XRemoteSignalReceiver;
+
+XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx);
+void xremote_signal_receiver_free(XRemoteSignalReceiver* rx_ctx);
+
+void xremote_signal_receiver_set_context(XRemoteSignalReceiver* rx_ctx, void *context, XRemoteClearCallback on_clear);
+void xremote_signal_receiver_set_rx_callback(XRemoteSignalReceiver* rx_ctx, XRemoteRxCallback rx_callback);
+InfraredSignal* xremote_signal_receiver_get_signal(XRemoteSignalReceiver *rx_ctx);
+
+void xremote_signal_receiver_start(XRemoteSignalReceiver *rx_ctx);
+void xremote_signal_receiver_stop(XRemoteSignalReceiver *rx_ctx);
+
+void xremote_signal_receiver_pause(XRemoteSignalReceiver *rx_ctx);
+void xremote_signal_receiver_resume(XRemoteSignalReceiver *rx_ctx);