فهرست منبع

update xremote

MX 2 سال پیش
والد
کامیت
e006f80578

+ 16 - 5
README.md

@@ -1,8 +1,8 @@
 # flipper-xremote
 Advanced IR Remote App for Flipper Device 
 
-## Idea
-With the current infrared application, users must navigate through the menu to locate each button individually. This requires scrolling to the desired button and selecting it, which can be uncomfortable. The idea behind `XRemote` is that all physical buttons are pre-mapped to specific category buttons, and pressing a physical button directly sends an infrared signal. This allows the flipper device to be used as a remote rather than as a tool that has a remote.
+## About
+Navigation to the menu to press each button individually can be often uncomfortable because it requires scrolling to the desired button and selecting it. The idea behind `XRemote` is that all physical buttons are pre-mapped to specific category buttons, and a physical button directly sends an infrared signal. This allows the flipper device to be used as a remote rather than as a tool that has a remote.
 
 `XRemote` also introduces a more user-friendly learning approach. Instead of having to manually name each button on the flipper when cloning a remote, the learning tool informs you upfront which buttons it will record. All you need to do is press the corresponding button on your existing remote, eliminating the need to name them individually.
 
@@ -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>

+ 2 - 2
application.fam

@@ -1,5 +1,5 @@
 App(
-    appid="xremote",
+    appid="flipper_xremote",
     name="XRemote",
     apptype=FlipperAppType.EXTERNAL,
     entry_point="xremote_main",
@@ -13,5 +13,5 @@ App(
     fap_icon="assets/App_Icon_10x10.png",
     fap_author="@kala13x",
     fap_weburl="https://github.com/kala13x/flipper-xremote",
-    fap_description="Advanced infrared remote control",
+    fap_description="Advanced infrared remote application",
 )

+ 40 - 0
docs/README.md

@@ -0,0 +1,40 @@
+## flipper-xremote
+Advanced IR Remote App for Flipper Device 
+
+## About 
+Navigation to the menu to press each button individually can be often uncomfortable because it requires scrolling to the desired button and selecting it. The idea behind XRemote is that all physical buttons are pre-mapped to specific category buttons, and a physical button directly sends an infrared signal. This allows the flipper device to be used as a remote rather than as a tool that has a remote.
+
+XRemote also introduces a more user-friendly learning approach. Instead of having to manually name each button on the flipper when cloning a remote, the learning tool informs you upfront which buttons it will record. All you need to do is press the corresponding button on your existing remote, eliminating the need to name them individually.
+
+The application is compatible with standard .ir files. However, to ensure functionality, names within these files must align with the predefined naming scheme. If the button is not highlighted when pressed or the notification LED does not light up, the button with the appropriate name cannot be found in the file.
+
+## Button schema
+Button name | Description
+------------|-------------------
+Power       | Power
+Setup       | Setup/Settings
+Input       | Input/Source
+Menu        | Menu
+List        | List
+Info        | Info
+Mode        | Mode
+Back        | Back
+Ok          | Enter/Ok
+Up          | Up
+Down        | Down
+Left        | Left
+Right       | Right
+Mute        | Mute
+Vol_up      | Volume up
+Vol_dn      | Volume down
+Ch_next     | Next channel
+Ch_prev     | Previous channel
+Next        | Jump forward
+Prev        | Jump backward
+Fast_fo     | Fast forward
+Fast_ba     | Fast backward
+Play_pa     | Play/Pause
+Pause       | Pause
+Play        | Play
+Stop        | Stop
+

+ 15 - 0
docs/changelog.md

@@ -0,0 +1,15 @@
+## v1.0
+
+First stable release
+- Adjusted layout of remote apps
+- Added learn mode support
+- Added signal analyzer app
+- Refactored the codebase
+
+## v0.9
+
+First beta release
+- Stable saved remote control apps
+- Flipper standard .ir file support
+- Horizontal/Vertical view support for all apps
+- Settings variable item list and functionality

+ 17 - 1
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"
@@ -95,7 +97,8 @@ bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* nam
     return false;
 }
 
-InfraredRemoteButton* infrared_remote_get_button_by_name(InfraredRemote* remote, const char* name) {
+InfraredRemoteButton*
+    infrared_remote_get_button_by_name(InfraredRemote* remote, const char* name) {
     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)) {
@@ -113,6 +116,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 +138,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/saved_remote_apps.png


BIN
screens/saved_remote_menu.png


BIN
screens/settings_menu.png


+ 9 - 17
views/xremote_about_view.c

@@ -9,8 +9,7 @@
 #include "xremote_about_view.h"
 #include "../xremote.h"
 
-static void xremote_about_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model)
-{
+static void xremote_about_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model) {
     UNUSED(model);
     char version[32];
 
@@ -30,8 +29,7 @@ static void xremote_about_view_draw_vertical(Canvas* canvas, XRemoteViewModel* m
     canvas_draw_str_aligned(canvas, 11, 100, AlignLeft, AlignTop, "gmail.com");
 }
 
-static void xremote_about_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model)
-{
+static void xremote_about_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model) {
     UNUSED(model);
     char version[32];
 
@@ -51,33 +49,27 @@ static void xremote_about_view_draw_horizontal(Canvas* canvas, XRemoteViewModel*
     canvas_draw_str_aligned(canvas, 10, 52, AlignLeft, AlignTop, "gmail.com");
 }
 
-static void xremote_about_view_draw_callback(Canvas* canvas, void* context)
-{
+static void xremote_about_view_draw_callback(Canvas* canvas, void* context) {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteAppContext* app_ctx = model->context;
     XRemoteViewDrawFunction xremote_about_view_draw_body;
 
     ViewOrientation orientation = app_ctx->app_settings->orientation;
     xremote_about_view_draw_body = orientation == ViewOrientationVertical ?
-        xremote_about_view_draw_vertical : xremote_about_view_draw_horizontal;
+                                       xremote_about_view_draw_vertical :
+                                       xremote_about_view_draw_horizontal;
 
     xremote_canvas_draw_header(canvas, orientation, "About");
     xremote_about_view_draw_body(canvas, model);
     xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit");
 }
 
-XRemoteView* xremote_about_view_alloc(void* app_ctx)
-{
-    XRemoteView *view = xremote_view_alloc(app_ctx,
-        NULL, xremote_about_view_draw_callback);
+XRemoteView* xremote_about_view_alloc(void* app_ctx) {
+    XRemoteView* view = xremote_view_alloc(app_ctx, NULL, xremote_about_view_draw_callback);
 
     with_view_model(
-        xremote_view_get_view(view),
-        XRemoteViewModel* model,
-        { model->context = app_ctx; },
-        true
-    );
+        xremote_view_get_view(view), XRemoteViewModel * model, { model->context = app_ctx; }, true);
 
     return view;
 }

+ 119 - 109
views/xremote_common_view.c

@@ -9,15 +9,54 @@
 #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;
+    void* context;
 };
 
-XRemoteView* xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewDrawCallback draw_cb)
-{
+XRemoteView*
+    xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewDrawCallback draw_cb) {
     XRemoteView* remote_view = malloc(sizeof(XRemoteView));
     remote_view->app_ctx = app_ctx;
     remote_view->view = view_alloc();
@@ -25,7 +64,8 @@ XRemoteView* xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewD
     remote_view->context = NULL;
     remote_view->on_clear = NULL;
 
-    view_set_orientation(remote_view->view, ((XRemoteAppContext*)app_ctx)->app_settings->orientation);
+    view_set_orientation(
+        remote_view->view, ((XRemoteAppContext*)app_ctx)->app_settings->orientation);
     view_allocate_model(remote_view->view, ViewModelTypeLocking, sizeof(XRemoteViewModel));
 
     view_set_input_callback(remote_view->view, input_cb);
@@ -35,61 +75,49 @@ XRemoteView* xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewD
     return remote_view;
 }
 
-void xremote_view_clear_context(XRemoteView* rview)
-{
+void xremote_view_clear_context(XRemoteView* rview) {
     furi_assert(rview);
 
-    if (rview->context != NULL &&
-        rview->on_clear != NULL)
-    {
-        rview->on_clear(rview->context);
-        rview->context = NULL;
-    }
+    if(rview->context && rview->on_clear) rview->on_clear(rview->context);
+
+    rview->context = NULL;
 }
 
-void xremote_view_set_context(XRemoteView* rview, void *context, XRemoteViewClearCallback on_clear)
-{
-    furi_assert(rview);
+void xremote_view_set_context(XRemoteView* rview, void* context, XRemoteClearCallback on_clear) {
     xremote_view_clear_context(rview);
     rview->context = context;
     rview->on_clear = on_clear;
 }
 
-void* xremote_view_get_context(XRemoteView* rview)
-{
+void* xremote_view_get_context(XRemoteView* rview) {
     furi_assert(rview);
     return rview->context;
 }
 
-void* xremote_view_get_app_context(XRemoteView* rview)
-{
+void* xremote_view_get_app_context(XRemoteView* rview) {
     furi_assert(rview);
     return rview->app_ctx;
 }
 
-void xremote_view_free(XRemoteView* rview)
-{
+void xremote_view_free(XRemoteView* rview) {
     furi_assert(rview);
     xremote_view_clear_context(rview);
     view_free(rview->view);
     free(rview);
 }
 
-View* xremote_view_get_view(XRemoteView* rview)
-{
+View* xremote_view_get_view(XRemoteView* rview) {
     furi_assert(rview);
     return rview->view;
 }
 
-InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView *rview, const char* name)
-{
+InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView* rview, const char* name) {
     xremote_app_assert(rview->context, NULL);
     InfraredRemote* remote = (InfraredRemote*)rview->context;
     return infrared_remote_get_button_by_name(remote, name);
 }
 
-bool xremote_view_press_button(XRemoteView *rview, InfraredRemoteButton* button)
-{
+bool xremote_view_press_button(XRemoteView* rview, InfraredRemoteButton* button) {
     xremote_app_assert(button, false);
     XRemoteAppSettings* settings = rview->app_ctx->app_settings;
 
@@ -102,141 +130,107 @@ bool xremote_view_press_button(XRemoteView *rview, InfraredRemoteButton* button)
     return true;
 }
 
-bool xremote_view_send_ir_msg_by_name(XRemoteView *rview, const char *name)
-{
+bool xremote_view_send_ir_msg_by_name(XRemoteView* rview, const char* name) {
     InfraredRemoteButton* button = xremote_view_get_button_by_name(rview, name);
     return (button != NULL) ? xremote_view_press_button(rview, button) : false;
 }
 
-void xremote_canvas_draw_icon(Canvas* canvas, uint8_t x, uint8_t y, XRemoteIcon icon)
-{
-    if (icon == XRemoteIconEnter)
-    {
+void xremote_canvas_draw_icon(Canvas* canvas, uint8_t x, uint8_t y, XRemoteIcon icon) {
+    if(icon == XRemoteIconEnter) {
         canvas_draw_circle(canvas, x - 2, y, 4);
         canvas_draw_disc(canvas, x - 2, y, 2);
-    }
-    else if (icon == XRemoteIconBack)
-    {
+    } else if(icon == XRemoteIconBack) {
         canvas_draw_triangle(canvas, x - 4, y - 2, 5, 3, CanvasDirectionRightToLeft);
         canvas_draw_line(canvas, x + 2, y + 1, x + 1, y + 2);
         canvas_draw_line(canvas, x + 2, y, x + 1, y - 1);
         canvas_draw_line(canvas, x, y - 2, x - 5, y - 2);
         canvas_draw_line(canvas, x, y + 3, x - 3, y + 3);
-    }
-    else if (icon == XRemoteIconArrowUp)
-    {
+    } else if(icon == XRemoteIconArrowUp) {
         canvas_draw_triangle(canvas, x - 2, y - 2, 5, 3, CanvasDirectionBottomToTop);
         canvas_draw_line(canvas, x - 2, y - 3, x - 2, y + 4);
-    }
-    else if (icon == XRemoteIconArrowDown)
-    {
+    } else if(icon == XRemoteIconArrowDown) {
         canvas_draw_triangle(canvas, x - 2, y + 2, 5, 3, CanvasDirectionTopToBottom);
         canvas_draw_line(canvas, x - 2, y - 4, x - 2, y + 3);
-    }
-    else if (icon == XRemoteIconArrowLeft)
-    {
+    } else if(icon == XRemoteIconArrowLeft) {
         canvas_draw_triangle(canvas, x - 4, y, 5, 3, CanvasDirectionRightToLeft);
         canvas_draw_line(canvas, x + 2, y, x - 5, y);
-    }
-    else if (icon == XRemoteIconArrowRight)
-    {
+    } else if(icon == XRemoteIconArrowRight) {
         canvas_draw_triangle(canvas, x, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_line(canvas, x - 6, y, x + 1, y);
-    }
-    else if (icon == XRemoteIconJumpForward)
-    {
+    } else if(icon == XRemoteIconJumpForward) {
         canvas_draw_triangle(canvas, x - 2, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_triangle(canvas, x - 5, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_line(canvas, x + 1, y - 2, x + 1, y + 2);
         canvas_draw_line(canvas, x - 4, y, x, y);
-    }
-    else if (icon == XRemoteIconJumpBackward)
-    {
+    } else if(icon == XRemoteIconJumpBackward) {
         canvas_draw_triangle(canvas, x - 2, y, 5, 3, CanvasDirectionRightToLeft);
         canvas_draw_triangle(canvas, x + 1, y, 5, 3, CanvasDirectionRightToLeft);
         canvas_draw_line(canvas, x - 5, y - 2, x - 5, y + 2);
         canvas_draw_line(canvas, x, y, x - 4, y);
-    }
-    else if (icon == XRemoteIconFastForward)
-    {
+    } else if(icon == XRemoteIconFastForward) {
         canvas_draw_triangle(canvas, x - 1, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_triangle(canvas, x - 4, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_line(canvas, x - 3, y, x, y);
-    }
-    else if (icon == XRemoteIconFastBackward)
-    {
+    } else if(icon == XRemoteIconFastBackward) {
         canvas_draw_triangle(canvas, x - 3, y, 5, 3, CanvasDirectionRightToLeft);
         canvas_draw_triangle(canvas, x, y, 5, 3, CanvasDirectionRightToLeft);
         canvas_draw_line(canvas, x - 1, y, x - 4, y);
-    }
-    else if (icon == XRemoteIconPlayPause)
-    {
+    } else if(icon == XRemoteIconPlayPause) {
         canvas_draw_triangle(canvas, x - 5, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_dot(canvas, x - 4, y);
         canvas_draw_line(canvas, x - 1, y - 2, x - 1, y + 2);
         canvas_draw_line(canvas, x + 1, y - 2, x + 1, y + 2);
-    }
-    else if (icon == XRemoteIconPlay)
-    {
+    } else if(icon == XRemoteIconPlay) {
         canvas_draw_triangle(canvas, x - 3, y, 5, 3, CanvasDirectionLeftToRight);
         canvas_draw_dot(canvas, x - 2, y);
-    }
-    else if (icon == XRemoteIconPause)
-    {
+    } else if(icon == XRemoteIconPause) {
         canvas_draw_line(canvas, x - 3, y - 2, x - 3, y + 2);
         canvas_draw_line(canvas, x - 1, y - 2, x - 1, y + 2);
-    }
-    else if (icon == XRemoteIconStop)
-    {
+    } else if(icon == XRemoteIconStop) {
         canvas_draw_box(canvas, x - 4, y - 2, 5, 5);
-    }
-    else if (icon == XRemoteIconOk)
-    {
+    } else if(icon == XRemoteIconOk) {
         canvas_draw_str(canvas, x - 7, y + 4, "OK");
     }
 }
 
-void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const char* section)
-{
+void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const char* section) {
     Align align = AlignLeft;
     uint8_t x = 0;
 
-    if (orient == ViewOrientationHorizontal)
-    {
+    if(orient == ViewOrientationHorizontal) {
         align = AlignRight;
         x = 128;
     }
 
     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)
-{
+void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char* text) {
     canvas_set_font(canvas, FontSecondary);
 
-    if (orient == ViewOrientationVertical)
-    {
+    if(orient == ViewOrientationVertical) {
         xremote_canvas_draw_icon(canvas, 6, 124, XRemoteIconBack);
         elements_multiline_text_aligned(canvas, 12, 128, AlignLeft, AlignBottom, text);
-    }
-    else
-    {
+    } else {
         uint8_t x = strncmp(text, "Hold", 4) ? 71 : 76;
         xremote_canvas_draw_icon(canvas, x, 60, XRemoteIconBack);
         elements_multiline_text_aligned(canvas, 128, 64, AlignRight, AlignBottom, text);
     }
 }
 
-void xremote_canvas_draw_button(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, XRemoteIcon icon)
-{
+void xremote_canvas_draw_button(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    XRemoteIcon icon) {
     canvas_draw_icon(canvas, x, y, &I_Button_18x18);
 
-    if (pressed)
-    {
+    if(pressed) {
         elements_slightly_rounded_box(canvas, x + 3, y + 2, 13, 13);
         canvas_set_color(canvas, ColorWhite);
     }
@@ -245,12 +239,15 @@ void xremote_canvas_draw_button(Canvas* canvas, bool pressed, uint8_t x, uint8_t
     canvas_set_color(canvas, ColorBlack);
 }
 
-void xremote_canvas_draw_button_png(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, const Icon* icon)
-{
+void xremote_canvas_draw_button_png(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    const Icon* icon) {
     canvas_draw_icon(canvas, x, y, &I_Button_18x18);
 
-    if (pressed)
-    {
+    if(pressed) {
         elements_slightly_rounded_box(canvas, x + 3, y + 2, 13, 13);
         canvas_set_color(canvas, ColorWhite);
     }
@@ -259,12 +256,16 @@ void xremote_canvas_draw_button_png(Canvas* canvas, bool pressed, uint8_t x, uin
     canvas_set_color(canvas, ColorBlack);
 }
 
-void xremote_canvas_draw_button_wide(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, char* text, XRemoteIcon icon)
-{
+void xremote_canvas_draw_button_wide(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    char* text,
+    XRemoteIcon icon) {
     elements_slightly_rounded_frame(canvas, x + 4, y, 56, 15);
 
-    if (pressed)
-    {
+    if(pressed) {
         elements_slightly_rounded_box(canvas, x + 6, y + 2, 52, 11);
         canvas_set_color(canvas, ColorWhite);
     }
@@ -274,12 +275,17 @@ void xremote_canvas_draw_button_wide(Canvas* canvas, bool pressed, uint8_t x, ui
     canvas_set_color(canvas, ColorBlack);
 }
 
-void xremote_canvas_draw_button_size(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, uint8_t xy, char* text, XRemoteIcon icon)
-{
+void xremote_canvas_draw_button_size(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    uint8_t xy,
+    char* text,
+    XRemoteIcon icon) {
     elements_slightly_rounded_frame(canvas, x + 4, y, xy, 15);
 
-    if (pressed)
-    {
+    if(pressed) {
         elements_slightly_rounded_box(canvas, x + 6, y + 2, xy - 4, 11);
         canvas_set_color(canvas, ColorWhite);
     }
@@ -289,12 +295,16 @@ void xremote_canvas_draw_button_size(Canvas* canvas, bool pressed, uint8_t x, ui
     canvas_set_color(canvas, ColorBlack);
 }
 
-void xremote_canvas_draw_frame(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, uint8_t xl, const char *text)
-{
+void xremote_canvas_draw_frame(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    uint8_t xl,
+    const char* text) {
     elements_slightly_rounded_frame(canvas, x, y, xl, 15);
 
-    if (pressed)
-    {
+    if(pressed) {
         elements_slightly_rounded_box(canvas, x + 2, y + 2, xl - 4, 11);
         canvas_set_color(canvas, ColorWhite);
     }

+ 89 - 39
views/xremote_common_view.h

@@ -20,32 +20,45 @@
 
 #include "../infrared/infrared_remote.h"
 
-#define XREMOTE_COMMAND_POWER           "Power"
-#define XREMOTE_COMMAND_SETUP           "Setup"
-#define XREMOTE_COMMAND_INPUT           "Input"
-#define XREMOTE_COMMAND_MENU            "Menu"
-#define XREMOTE_COMMAND_LIST            "List"
-#define XREMOTE_COMMAND_INFO            "Info"
-#define XREMOTE_COMMAND_BACK            "Back"
-#define XREMOTE_COMMAND_OK              "Ok"
-#define XREMOTE_COMMAND_UP              "Up"
-#define XREMOTE_COMMAND_DOWN            "Down"
-#define XREMOTE_COMMAND_LEFT            "Left"
-#define XREMOTE_COMMAND_RIGHT           "Right"
-#define XREMOTE_COMMAND_JUMP_FORWARD    "Next"
-#define XREMOTE_COMMAND_JUMP_BACKWARD   "Prev"
-#define XREMOTE_COMMAND_FAST_FORWARD    "Fast_fo"
-#define XREMOTE_COMMAND_FAST_BACKWARD   "Fast_ba"
-#define XREMOTE_COMMAND_PLAY_PAUSE      "Play_pa"
-#define XREMOTE_COMMAND_PAUSE           "Pause"
-#define XREMOTE_COMMAND_PLAY            "Play"
-#define XREMOTE_COMMAND_STOP            "Stop"
-#define XREMOTE_COMMAND_MUTE            "Mute"
-#define XREMOTE_COMMAND_MODE            "Mode"
-#define XREMOTE_COMMAND_VOL_UP          "Vol_up"
-#define XREMOTE_COMMAND_VOL_DOWN        "Vol_dn"
-#define XREMOTE_COMMAND_NEXT_CHAN       "Ch_next"
-#define XREMOTE_COMMAND_PREV_CHAN       "Ch_prev"
+#define XREMOTE_BUTTON_COUNT 26
+#define XREMOTE_COMMAND_POWER "Power"
+#define XREMOTE_COMMAND_SETUP "Setup"
+#define XREMOTE_COMMAND_INPUT "Input"
+#define XREMOTE_COMMAND_MENU "Menu"
+#define XREMOTE_COMMAND_LIST "List"
+#define XREMOTE_COMMAND_INFO "Info"
+#define XREMOTE_COMMAND_BACK "Back"
+#define XREMOTE_COMMAND_OK "Ok"
+#define XREMOTE_COMMAND_UP "Up"
+#define XREMOTE_COMMAND_DOWN "Down"
+#define XREMOTE_COMMAND_LEFT "Left"
+#define XREMOTE_COMMAND_RIGHT "Right"
+#define XREMOTE_COMMAND_JUMP_FORWARD "Next"
+#define XREMOTE_COMMAND_JUMP_BACKWARD "Prev"
+#define XREMOTE_COMMAND_FAST_FORWARD "Fast_fo"
+#define XREMOTE_COMMAND_FAST_BACKWARD "Fast_ba"
+#define XREMOTE_COMMAND_PLAY_PAUSE "Play_pa"
+#define XREMOTE_COMMAND_PAUSE "Pause"
+#define XREMOTE_COMMAND_PLAY "Play"
+#define XREMOTE_COMMAND_STOP "Stop"
+#define XREMOTE_COMMAND_MUTE "Mute"
+#define XREMOTE_COMMAND_MODE "Mode"
+#define XREMOTE_COMMAND_VOL_UP "Vol_up"
+#define XREMOTE_COMMAND_VOL_DOWN "Vol_dn"
+#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 */
@@ -80,11 +93,15 @@ typedef struct {
 
 typedef enum {
     XRemoteViewNone,
+    XRemoteViewSignal,
+    XRemoteViewTextInput,
+    XRemoteViewDialogExit,
 
     /* Main page */
     XRemoteViewSubmenu,
     XRemoteViewLearn,
     XRemoteViewSaved,
+    XRemoteViewAnalyzer,
     XRemoteViewSettings,
     XRemoteViewAbout,
 
@@ -98,27 +115,60 @@ 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);
+void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char* text);
 
 void xremote_canvas_draw_icon(Canvas* canvas, uint8_t x, uint8_t y, XRemoteIcon icon);
-void xremote_canvas_draw_button(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, XRemoteIcon icon);
-void xremote_canvas_draw_button_png(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, const Icon* icon);
-void xremote_canvas_draw_button_wide(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, char* text, XRemoteIcon icon);
-void xremote_canvas_draw_button_size(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, uint8_t xy, char* text, XRemoteIcon icon);
-void xremote_canvas_draw_frame(Canvas* canvas, bool pressed, uint8_t x, uint8_t y, uint8_t xl, const char *text);
-
-XRemoteView* xremote_view_alloc(void *app_ctx, ViewInputCallback input_cb, ViewDrawCallback draw_cb);
+void xremote_canvas_draw_button(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    XRemoteIcon icon);
+void xremote_canvas_draw_button_png(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    const Icon* icon);
+void xremote_canvas_draw_button_wide(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    char* text,
+    XRemoteIcon icon);
+void xremote_canvas_draw_button_size(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    uint8_t xy,
+    char* text,
+    XRemoteIcon icon);
+void xremote_canvas_draw_frame(
+    Canvas* canvas,
+    bool pressed,
+    uint8_t x,
+    uint8_t y,
+    uint8_t xl,
+    const char* text);
+
+XRemoteView*
+    xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewDrawCallback draw_cb);
 void xremote_view_free(XRemoteView* rview);
 
-InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView *rview, const char* name);
-bool xremote_view_press_button(XRemoteView *rview, InfraredRemoteButton* button);
-bool xremote_view_send_ir_msg_by_name(XRemoteView *rview, const char *name);
+InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView* rview, const char* name);
+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);

+ 55 - 74
views/xremote_control_view.c

@@ -9,9 +9,8 @@
 #include "xremote_control_view.h"
 #include "../xremote_app.h"
 
-static void xremote_control_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model)
-{
-    XRemoteAppContext *app_ctx = model->context;
+static void xremote_control_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model) {
+    XRemoteAppContext* app_ctx = model->context;
 
     xremote_canvas_draw_button_png(canvas, model->up_pressed, 23, 30, &I_Chanup_Icon_11x11);
     xremote_canvas_draw_button_png(canvas, model->down_pressed, 23, 72, &I_Chandown_Icon_11x11);
@@ -20,13 +19,12 @@ static void xremote_control_view_draw_vertical(Canvas* canvas, XRemoteViewModel*
     xremote_canvas_draw_button_png(canvas, model->back_pressed, 2, 95, &I_Mute_Icon_11x11);
     xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 51, XRemoteIconPlayPause);
 
-    if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
+    if(app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 22, 107, &I_Hold_Text_17x4);
 }
 
-static void xremote_control_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model)
-{
-    XRemoteAppContext *app_ctx = model->context;
+static void xremote_control_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model) {
+    XRemoteAppContext* app_ctx = model->context;
 
     xremote_canvas_draw_button_png(canvas, model->up_pressed, 23, 2, &I_Chanup_Icon_11x11);
     xremote_canvas_draw_button_png(canvas, model->down_pressed, 23, 44, &I_Chandown_Icon_11x11);
@@ -35,120 +33,104 @@ static void xremote_control_view_draw_horizontal(Canvas* canvas, XRemoteViewMode
     xremote_canvas_draw_button_png(canvas, model->back_pressed, 70, 33, &I_Mute_Icon_11x11);
     xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 23, XRemoteIconPlayPause);
 
-    if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
+    if(app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 90, 45, &I_Hold_Text_17x4);
 }
 
-static void xremote_control_view_draw_callback(Canvas* canvas, void* context)
-{
+static void xremote_control_view_draw_callback(Canvas* canvas, void* context) {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteAppContext* app_ctx = model->context;
     ViewOrientation orientation = app_ctx->app_settings->orientation;
-    const char *exit_str = xremote_app_context_get_exit_str(app_ctx);
+    const char* exit_str = xremote_app_context_get_exit_str(app_ctx);
 
     XRemoteViewDrawFunction xremote_control_view_draw_body;
     xremote_control_view_draw_body = orientation == ViewOrientationVertical ?
-        xremote_control_view_draw_vertical : xremote_control_view_draw_horizontal;
+                                         xremote_control_view_draw_vertical :
+                                         xremote_control_view_draw_horizontal;
 
     xremote_canvas_draw_header(canvas, orientation, "Control");
     xremote_control_view_draw_body(canvas, model);
     xremote_canvas_draw_exit_footer(canvas, orientation, exit_str);
 }
 
-static void xremote_control_view_process(XRemoteView* view, InputEvent* event)
-{
+static void xremote_control_view_process(XRemoteView* view, InputEvent* event) {
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             XRemoteAppContext* app_ctx = xremote_view_get_app_context(view);
             XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
             InfraredRemoteButton* button = NULL;
             model->context = app_ctx;
 
-            if (event->type == InputTypePress)
-            {
-                if (event->key == InputKeyOk)
-                {
+            if(event->type == InputTypePress) {
+                if(event->key == InputKeyOk) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY_PAUSE);
-                    if (xremote_view_press_button(view, button)) model->ok_pressed = true;
-                }
-                else if (event->key == InputKeyUp)
-                {
+                    if(xremote_view_press_button(view, button)) model->ok_pressed = true;
+                } else if(event->key == InputKeyUp) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_NEXT_CHAN);
-                    if (xremote_view_press_button(view, button)) model->up_pressed = true;
-                }
-                else if (event->key == InputKeyDown)
-                {
+                    if(xremote_view_press_button(view, button)) model->up_pressed = true;
+                } else if(event->key == InputKeyDown) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PREV_CHAN);
-                    if (xremote_view_press_button(view, button)) model->down_pressed = true;
-                }
-                else if (event->key == InputKeyLeft)
-                {
+                    if(xremote_view_press_button(view, button)) model->down_pressed = true;
+                } else if(event->key == InputKeyLeft) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_VOL_DOWN);
-                    if (xremote_view_press_button(view, button)) model->left_pressed = true;
-                }
-                else if (event->key == InputKeyRight)
-                {
+                    if(xremote_view_press_button(view, button)) model->left_pressed = true;
+                } else if(event->key == InputKeyRight) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_VOL_UP);
-                    if (xremote_view_press_button(view, button)) model->right_pressed = true;
+                    if(xremote_view_press_button(view, button)) model->right_pressed = true;
                 }
-            }
-            else if (event->type == InputTypeShort &&
-                    event->key == InputKeyBack &&
-                    exit == XRemoteAppExitHold)
-            {
+            } else if(
+                event->type == InputTypeShort && event->key == InputKeyBack &&
+                exit == XRemoteAppExitHold) {
                 button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_MUTE);
-                if (xremote_view_press_button(view, button)) model->back_pressed = true;
-            }
-            else if (event->type == InputTypeLong &&
-                    event->key == InputKeyBack &&
-                    exit == XRemoteAppExitPress)
-            {
+                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_MUTE);
-                if (xremote_view_press_button(view, button)) model->back_pressed = true;
-            }
-            else if (event->type == InputTypeRelease)
-            {
-                if (event->key == InputKeyOk) model->ok_pressed = false;
-                else if (event->key == InputKeyUp) model->up_pressed = false;
-                else if (event->key == InputKeyDown) model->down_pressed = false;
-                else if (event->key == InputKeyLeft) model->left_pressed = false;
-                else if (event->key == InputKeyRight) model->right_pressed = false;
-                else if (event->key == InputKeyBack) model->back_pressed = false;
+                if(xremote_view_press_button(view, button)) model->back_pressed = true;
+            } else if(event->type == InputTypeRelease) {
+                if(event->key == InputKeyOk)
+                    model->ok_pressed = false;
+                else if(event->key == InputKeyUp)
+                    model->up_pressed = false;
+                else if(event->key == InputKeyDown)
+                    model->down_pressed = false;
+                else if(event->key == InputKeyLeft)
+                    model->left_pressed = false;
+                else if(event->key == InputKeyRight)
+                    model->right_pressed = false;
+                else if(event->key == InputKeyBack)
+                    model->back_pressed = false;
             }
         },
         true);
 }
 
-static bool xremote_control_view_input_callback(InputEvent* event, void* context)
-{
+static bool xremote_control_view_input_callback(InputEvent* event, void* context) {
     furi_assert(context);
     XRemoteView* view = (XRemoteView*)context;
     XRemoteAppContext* app_ctx = xremote_view_get_app_context(view);
     XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
 
-    if (event->key == InputKeyBack &&
-        event->type == InputTypeShort &&
-        exit == XRemoteAppExitPress) return false;
-    else if (event->key == InputKeyBack &&
-        event->type == InputTypeLong &&
-        exit == XRemoteAppExitHold) return false;
+    if(event->key == InputKeyBack && event->type == InputTypeShort && exit == XRemoteAppExitPress)
+        return false;
+    else if(event->key == InputKeyBack && event->type == InputTypeLong && exit == XRemoteAppExitHold)
+        return false;
 
     xremote_control_view_process(view, event);
     return true;
 }
 
-XRemoteView* xremote_control_view_alloc(void *app_ctx)
-{
-    XRemoteView *view = xremote_view_alloc(app_ctx,
-        xremote_control_view_input_callback,
-        xremote_control_view_draw_callback);
+XRemoteView* xremote_control_view_alloc(void* app_ctx) {
+    XRemoteView* view = xremote_view_alloc(
+        app_ctx, xremote_control_view_input_callback, xremote_control_view_draw_callback);
 
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             model->context = app_ctx;
             model->up_pressed = false;
@@ -158,8 +140,7 @@ XRemoteView* xremote_control_view_alloc(void *app_ctx)
             model->back_pressed = false;
             model->ok_pressed = false;
         },
-        true
-    );
+        true);
 
     return view;
 }

+ 5 - 12
views/xremote_custom_view.c

@@ -9,11 +9,10 @@
 #include "xremote_custom_view.h"
 #include "../xremote_app.h"
 
-static void xremote_custom_view_draw_callback(Canvas* canvas, void* context)
-{
+static void xremote_custom_view_draw_callback(Canvas* canvas, void* context) {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteAppContext* app_ctx = model->context;
 
     ViewOrientation orientation = app_ctx->app_settings->orientation;
     uint64_t x = orientation == ViewOrientationVertical ? 70 : 34;
@@ -24,17 +23,11 @@ static void xremote_custom_view_draw_callback(Canvas* canvas, void* context)
     xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit");
 }
 
-XRemoteView* xremote_custom_view_alloc(void* app_ctx)
-{
-    XRemoteView *view = xremote_view_alloc(app_ctx,
-        NULL, xremote_custom_view_draw_callback);
+XRemoteView* xremote_custom_view_alloc(void* app_ctx) {
+    XRemoteView* view = xremote_view_alloc(app_ctx, NULL, xremote_custom_view_draw_callback);
 
     with_view_model(
-        xremote_view_get_view(view),
-        XRemoteViewModel* model,
-        { model->context = app_ctx; },
-        true
-    );
+        xremote_view_get_view(view), XRemoteViewModel * model, { model->context = app_ctx; }, true);
 
     return view;
 }

+ 49 - 57
views/xremote_general_view.c

@@ -9,108 +9,101 @@
 #include "xremote_general_view.h"
 #include "../xremote_app.h"
 
-static void xremote_general_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model)
-{
+static void xremote_general_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model) {
     xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 27, "Power", XRemoteIconEnter);
     xremote_canvas_draw_button_wide(canvas, model->up_pressed, 0, 45, "Input", XRemoteIconArrowUp);
-    xremote_canvas_draw_button_wide(canvas, model->down_pressed, 0, 63, "Setup", XRemoteIconArrowDown);
-    xremote_canvas_draw_button_wide(canvas, model->left_pressed, 0, 81, "Menu", XRemoteIconArrowLeft);
-    xremote_canvas_draw_button_wide(canvas, model->right_pressed, 0, 99, "List", XRemoteIconArrowRight);
+    xremote_canvas_draw_button_wide(
+        canvas, model->down_pressed, 0, 63, "Setup", XRemoteIconArrowDown);
+    xremote_canvas_draw_button_wide(
+        canvas, model->left_pressed, 0, 81, "Menu", XRemoteIconArrowLeft);
+    xremote_canvas_draw_button_wide(
+        canvas, model->right_pressed, 0, 99, "List", XRemoteIconArrowRight);
 }
 
-static void xremote_general_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model)
-{
+static void xremote_general_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model) {
     xremote_canvas_draw_button_wide(canvas, model->ok_pressed, 0, 7, "Power", XRemoteIconEnter);
     xremote_canvas_draw_button_wide(canvas, model->up_pressed, 0, 25, "Input", XRemoteIconArrowUp);
-    xremote_canvas_draw_button_wide(canvas, model->down_pressed, 0, 43, "Setup", XRemoteIconArrowDown);
-    xremote_canvas_draw_button_wide(canvas, model->left_pressed, 64, 21, "Menu", XRemoteIconArrowLeft);
-    xremote_canvas_draw_button_wide(canvas, model->right_pressed, 64, 39, "List", XRemoteIconArrowRight);
+    xremote_canvas_draw_button_wide(
+        canvas, model->down_pressed, 0, 43, "Setup", XRemoteIconArrowDown);
+    xremote_canvas_draw_button_wide(
+        canvas, model->left_pressed, 64, 21, "Menu", XRemoteIconArrowLeft);
+    xremote_canvas_draw_button_wide(
+        canvas, model->right_pressed, 64, 39, "List", XRemoteIconArrowRight);
 }
 
-static void xremote_general_view_draw_callback(Canvas* canvas, void* context)
-{
+static void xremote_general_view_draw_callback(Canvas* canvas, void* context) {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteAppContext* app_ctx = model->context;
     XRemoteViewDrawFunction xremote_general_view_draw_body;
 
     ViewOrientation orientation = app_ctx->app_settings->orientation;
     xremote_general_view_draw_body = orientation == ViewOrientationVertical ?
-        xremote_general_view_draw_vertical : xremote_general_view_draw_horizontal;
+                                         xremote_general_view_draw_vertical :
+                                         xremote_general_view_draw_horizontal;
 
     xremote_canvas_draw_header(canvas, orientation, "General");
     xremote_general_view_draw_body(canvas, model);
     xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit");
 }
 
-static void xremote_general_view_process(XRemoteView* view, InputEvent* event)
-{
+static void xremote_general_view_process(XRemoteView* view, InputEvent* event) {
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
-            if (event->type == InputTypePress)
-            {
+            if(event->type == InputTypePress) {
                 model->context = xremote_view_get_app_context(view);
                 InfraredRemoteButton* button = NULL;
 
-                if (event->key == InputKeyOk)
-                {
+                if(event->key == InputKeyOk) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_POWER);
-                    if (xremote_view_press_button(view, button)) model->ok_pressed = true;
-                }
-                else if (event->key == InputKeyUp)
-                {
+                    if(xremote_view_press_button(view, button)) model->ok_pressed = true;
+                } else if(event->key == InputKeyUp) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_INPUT);
-                    if (xremote_view_press_button(view, button)) model->up_pressed = true;
-                }
-                else if (event->key == InputKeyDown)
-                {
+                    if(xremote_view_press_button(view, button)) model->up_pressed = true;
+                } else if(event->key == InputKeyDown) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_SETUP);
-                    if (xremote_view_press_button(view, button)) model->down_pressed = true;
-                }
-                else if (event->key == InputKeyLeft)
-                {
+                    if(xremote_view_press_button(view, button)) model->down_pressed = true;
+                } else if(event->key == InputKeyLeft) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_MENU);
-                    if (xremote_view_press_button(view, button)) model->left_pressed = true;
-                }
-                else if (event->key == InputKeyRight)
-                {
+                    if(xremote_view_press_button(view, button)) model->left_pressed = true;
+                } else if(event->key == InputKeyRight) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_LIST);
-                    if (xremote_view_press_button(view, button)) model->right_pressed = true;
+                    if(xremote_view_press_button(view, button)) model->right_pressed = true;
                 }
-            }
-            else if (event->type == InputTypeRelease)
-            {
-                if (event->key == InputKeyOk) model->ok_pressed = false;
-                else if (event->key == InputKeyUp) model->up_pressed = false;
-                else if (event->key == InputKeyDown) model->down_pressed = false;
-                else if (event->key == InputKeyLeft) model->left_pressed = false;
-                else if (event->key == InputKeyRight) model->right_pressed = false;
+            } else if(event->type == InputTypeRelease) {
+                if(event->key == InputKeyOk)
+                    model->ok_pressed = false;
+                else if(event->key == InputKeyUp)
+                    model->up_pressed = false;
+                else if(event->key == InputKeyDown)
+                    model->down_pressed = false;
+                else if(event->key == InputKeyLeft)
+                    model->left_pressed = false;
+                else if(event->key == InputKeyRight)
+                    model->right_pressed = false;
             }
         },
         true);
 }
 
-static bool xremote_general_view_input_callback(InputEvent* event, void* context)
-{
+static bool xremote_general_view_input_callback(InputEvent* event, void* context) {
     furi_assert(context);
     XRemoteView* view = (XRemoteView*)context;
-    if (event->key == InputKeyBack) return false;
+    if(event->key == InputKeyBack) return false;
 
     xremote_general_view_process(view, event);
     return true;
 }
 
-XRemoteView* xremote_general_view_alloc(void* app_ctx)
-{
-    XRemoteView *view = xremote_view_alloc(app_ctx,
-        xremote_general_view_input_callback,
-        xremote_general_view_draw_callback);
+XRemoteView* xremote_general_view_alloc(void* app_ctx) {
+    XRemoteView* view = xremote_view_alloc(
+        app_ctx, xremote_general_view_input_callback, xremote_general_view_draw_callback);
 
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             model->context = app_ctx;
             model->up_pressed = false;
@@ -120,8 +113,7 @@ XRemoteView* xremote_general_view_alloc(void* app_ctx)
             model->back_pressed = false;
             model->ok_pressed = false;
         },
-        true
-    );
+        true);
 
     return view;
 }

+ 210 - 16
views/xremote_learn_view.c

@@ -7,34 +7,228 @@
  */
 
 #include "xremote_learn_view.h"
+#include "../xremote_learn.h"
 #include "../xremote_app.h"
 
-static void xremote_learn_view_draw_callback(Canvas* canvas, void* context)
-{
+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 *view = xremote_view_alloc(app_ctx,
-        NULL, xremote_learn_view_draw_callback);
+XRemoteView* xremote_learn_view_alloc(void* app_ctx, void* learn_ctx) {
+    XRemoteView* view = xremote_view_alloc(
+        app_ctx, 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; },
-        true
-    );
+        XRemoteViewModel * model,
+        {
+            model->context = learn_ctx;
+            model->right_pressed = false;
+            model->back_pressed = false;
+            model->ok_pressed = false;
+        },
+        true);
 
     return view;
 }

+ 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);

+ 55 - 75
views/xremote_navigation_view.c

@@ -9,10 +9,8 @@
 #include "xremote_navigation_view.h"
 #include "../xremote_app.h"
 
-
-static void xremote_navigation_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model)
-{
-    XRemoteAppContext *app_ctx = model->context;
+static void xremote_navigation_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model) {
+    XRemoteAppContext* app_ctx = model->context;
 
     xremote_canvas_draw_button(canvas, model->up_pressed, 23, 30, XRemoteIconArrowUp);
     xremote_canvas_draw_button(canvas, model->down_pressed, 23, 72, XRemoteIconArrowDown);
@@ -21,13 +19,12 @@ static void xremote_navigation_view_draw_vertical(Canvas* canvas, XRemoteViewMod
     xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 51, XRemoteIconOk);
     xremote_canvas_draw_button(canvas, model->back_pressed, 2, 95, XRemoteIconBack);
 
-    if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
+    if(app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 22, 107, &I_Hold_Text_17x4);
 }
 
-static void xremote_navigation_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model)
-{
-    XRemoteAppContext *app_ctx = model->context;
+static void xremote_navigation_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model) {
+    XRemoteAppContext* app_ctx = model->context;
 
     xremote_canvas_draw_button(canvas, model->up_pressed, 23, 2, XRemoteIconArrowUp);
     xremote_canvas_draw_button(canvas, model->down_pressed, 23, 44, XRemoteIconArrowDown);
@@ -36,120 +33,104 @@ static void xremote_navigation_view_draw_horizontal(Canvas* canvas, XRemoteViewM
     xremote_canvas_draw_button(canvas, model->ok_pressed, 23, 23, XRemoteIconOk);
     xremote_canvas_draw_button(canvas, model->back_pressed, 70, 33, XRemoteIconBack);
 
-    if (app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
+    if(app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 90, 45, &I_Hold_Text_17x4);
 }
 
-static void xremote_navigation_view_draw_callback(Canvas* canvas, void* context)
-{
+static void xremote_navigation_view_draw_callback(Canvas* canvas, void* context) {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteAppContext* app_ctx = model->context;
     ViewOrientation orientation = app_ctx->app_settings->orientation;
-    const char *exit_str = xremote_app_context_get_exit_str(app_ctx);
+    const char* exit_str = xremote_app_context_get_exit_str(app_ctx);
 
     XRemoteViewDrawFunction xremote_navigation_view_draw_body;
     xremote_navigation_view_draw_body = orientation == ViewOrientationVertical ?
-        xremote_navigation_view_draw_vertical : xremote_navigation_view_draw_horizontal;
+                                            xremote_navigation_view_draw_vertical :
+                                            xremote_navigation_view_draw_horizontal;
 
     xremote_canvas_draw_header(canvas, orientation, "Navigation");
     xremote_navigation_view_draw_body(canvas, model);
     xremote_canvas_draw_exit_footer(canvas, orientation, exit_str);
 }
 
-static void xremote_navigation_view_process(XRemoteView* view, InputEvent* event)
-{
+static void xremote_navigation_view_process(XRemoteView* view, InputEvent* event) {
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             XRemoteAppContext* app_ctx = xremote_view_get_app_context(view);
             XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
             InfraredRemoteButton* button = NULL;
             model->context = app_ctx;
 
-            if (event->type == InputTypePress)
-            {
-                if (event->key == InputKeyUp)
-                {
+            if(event->type == InputTypePress) {
+                if(event->key == InputKeyUp) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_UP);
-                    if (xremote_view_press_button(view, button)) model->up_pressed = true;
-                }
-                else if (event->key == InputKeyDown)
-                {
+                    if(xremote_view_press_button(view, button)) model->up_pressed = true;
+                } else if(event->key == InputKeyDown) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_DOWN);
-                    if (xremote_view_press_button(view, button)) model->down_pressed = true;
-                }
-                else if (event->key == InputKeyLeft)
-                {
+                    if(xremote_view_press_button(view, button)) model->down_pressed = true;
+                } else if(event->key == InputKeyLeft) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_LEFT);
-                    if (xremote_view_press_button(view, button)) model->left_pressed = true;
-                }
-                else if (event->key == InputKeyRight)
-                {
+                    if(xremote_view_press_button(view, button)) model->left_pressed = true;
+                } else if(event->key == InputKeyRight) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_RIGHT);
-                    if (xremote_view_press_button(view, button)) model->right_pressed = true;
-                }
-                else if (event->key == InputKeyOk)
-                {
+                    if(xremote_view_press_button(view, button)) model->right_pressed = true;
+                } else if(event->key == InputKeyOk) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_OK);
-                    if (xremote_view_press_button(view, button)) model->ok_pressed = true;
+                    if(xremote_view_press_button(view, button)) model->ok_pressed = true;
                 }
-            }
-            else if (event->type == InputTypeShort &&
-                    event->key == InputKeyBack &&
-                    exit == XRemoteAppExitHold)
-            {
+            } else if(
+                event->type == InputTypeShort && event->key == InputKeyBack &&
+                exit == XRemoteAppExitHold) {
                 button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_BACK);
-                if (xremote_view_press_button(view, button)) model->back_pressed = true;
-            }
-            else if (event->type == InputTypeLong &&
-                    event->key == InputKeyBack &&
-                    exit == XRemoteAppExitPress)
-            {
+                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_BACK);
-                if (xremote_view_press_button(view, button)) model->back_pressed = true;
-            }
-            else if (event->type == InputTypeRelease)
-            {
-                if (event->key == InputKeyUp) model->up_pressed = false;
-                else if (event->key == InputKeyDown) model->down_pressed = false;
-                else if (event->key == InputKeyLeft) model->left_pressed = false;
-                else if (event->key == InputKeyRight) model->right_pressed = false;
-                else if (event->key == InputKeyOk) model->ok_pressed = false;
-                else if (event->key == InputKeyBack) model->back_pressed = false;
+                if(xremote_view_press_button(view, button)) model->back_pressed = true;
+            } else if(event->type == InputTypeRelease) {
+                if(event->key == InputKeyUp)
+                    model->up_pressed = false;
+                else if(event->key == InputKeyDown)
+                    model->down_pressed = false;
+                else if(event->key == InputKeyLeft)
+                    model->left_pressed = false;
+                else if(event->key == InputKeyRight)
+                    model->right_pressed = false;
+                else if(event->key == InputKeyOk)
+                    model->ok_pressed = false;
+                else if(event->key == InputKeyBack)
+                    model->back_pressed = false;
             }
         },
         true);
 }
 
-static bool xremote_navigation_view_input_callback(InputEvent* event, void* context)
-{
+static bool xremote_navigation_view_input_callback(InputEvent* event, void* context) {
     furi_assert(context);
     XRemoteView* view = (XRemoteView*)context;
     XRemoteAppContext* app_ctx = xremote_view_get_app_context(view);
     XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
 
-    if (event->key == InputKeyBack &&
-        event->type == InputTypeShort &&
-        exit == XRemoteAppExitPress) return false;
-    else if (event->key == InputKeyBack &&
-        event->type == InputTypeLong &&
-        exit == XRemoteAppExitHold) return false;
+    if(event->key == InputKeyBack && event->type == InputTypeShort && exit == XRemoteAppExitPress)
+        return false;
+    else if(event->key == InputKeyBack && event->type == InputTypeLong && exit == XRemoteAppExitHold)
+        return false;
 
     xremote_navigation_view_process(view, event);
     return true;
 }
 
-XRemoteView* xremote_navigation_view_alloc(void* app_ctx)
-{
-    XRemoteView *view = xremote_view_alloc(app_ctx,
-        xremote_navigation_view_input_callback,
-        xremote_navigation_view_draw_callback);
+XRemoteView* xremote_navigation_view_alloc(void* app_ctx) {
+    XRemoteView* view = xremote_view_alloc(
+        app_ctx, xremote_navigation_view_input_callback, xremote_navigation_view_draw_callback);
 
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             model->context = app_ctx;
             model->up_pressed = false;
@@ -159,8 +140,7 @@ XRemoteView* xremote_navigation_view_alloc(void* app_ctx)
             model->back_pressed = false;
             model->ok_pressed = false;
         },
-        true
-    );
+        true);
 
     return view;
 }

+ 62 - 81
views/xremote_player_view.c

@@ -9,146 +9,128 @@
 #include "xremote_player_view.h"
 #include "../xremote_app.h"
 
-static void xremote_player_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model)
-{
-    XRemoteAppContext *app_ctx = model->context;
+static void xremote_player_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model) {
+    XRemoteAppContext* app_ctx = model->context;
 
     xremote_canvas_draw_button(canvas, model->up_pressed, 23, 30, XRemoteIconJumpForward);
     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)
+    if(app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 22, 107, &I_Hold_Text_17x4);
 }
 
-static void xremote_player_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model)
-{
-    XRemoteAppContext *app_ctx = model->context;
+static void xremote_player_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model) {
+    XRemoteAppContext* app_ctx = model->context;
 
     xremote_canvas_draw_button(canvas, model->up_pressed, 23, 2, XRemoteIconJumpForward);
     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)
+    if(app_ctx->app_settings->exit_behavior == XRemoteAppExitPress)
         canvas_draw_icon(canvas, 90, 45, &I_Hold_Text_17x4);
 }
 
-static void xremote_player_view_draw_callback(Canvas* canvas, void* context)
-{
+static void xremote_player_view_draw_callback(Canvas* canvas, void* context) {
     furi_assert(context);
     XRemoteViewModel* model = context;
-    XRemoteAppContext *app_ctx = model->context;
+    XRemoteAppContext* app_ctx = model->context;
     ViewOrientation orientation = app_ctx->app_settings->orientation;
-    const char *exit_str = xremote_app_context_get_exit_str(app_ctx);
+    const char* exit_str = xremote_app_context_get_exit_str(app_ctx);
 
     XRemoteViewDrawFunction xremote_player_view_draw_body;
     xremote_player_view_draw_body = orientation == ViewOrientationVertical ?
-        xremote_player_view_draw_vertical : xremote_player_view_draw_horizontal;
+                                        xremote_player_view_draw_vertical :
+                                        xremote_player_view_draw_horizontal;
 
     xremote_canvas_draw_header(canvas, orientation, "Playback");
     xremote_player_view_draw_body(canvas, model);
     xremote_canvas_draw_exit_footer(canvas, orientation, exit_str);
 }
 
-static void xremote_player_view_process(XRemoteView* view, InputEvent* event)
-{
+static void xremote_player_view_process(XRemoteView* view, InputEvent* event) {
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             XRemoteAppContext* app_ctx = xremote_view_get_app_context(view);
             XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
             InfraredRemoteButton* button = NULL;
             model->context = app_ctx;
 
-            if (event->type == InputTypePress)
-            {
-                if (event->key == InputKeyUp)
-                {
+            if(event->type == InputTypePress) {
+                if(event->key == InputKeyUp) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_JUMP_FORWARD);
-                    if (xremote_view_press_button(view, button)) model->up_pressed = true;
-                }
-                else if (event->key == InputKeyDown)
-                {
+                    if(xremote_view_press_button(view, button)) model->up_pressed = true;
+                } else if(event->key == InputKeyDown) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_JUMP_BACKWARD);
-                    if (xremote_view_press_button(view, button)) model->down_pressed = true;
-                }
-                else if (event->key == InputKeyLeft)
-                {
+                    if(xremote_view_press_button(view, button)) model->down_pressed = true;
+                } else if(event->key == InputKeyLeft) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_FAST_BACKWARD);
-                    if (xremote_view_press_button(view, button)) model->left_pressed = true;
-                }
-                else if (event->key == InputKeyRight)
-                {
+                    if(xremote_view_press_button(view, button)) model->left_pressed = true;
+                } else if(event->key == InputKeyRight) {
                     button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_FAST_FORWARD);
-                    if (xremote_view_press_button(view, button)) model->right_pressed = true;
-                }
-                else if (event->key == InputKeyOk)
-                {
-                    button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PLAY);
-                    if (xremote_view_press_button(view, button)) model->ok_pressed = true;
+                    if(xremote_view_press_button(view, button)) model->right_pressed = true;
+                } else if(event->key == InputKeyOk) {
+                    button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE);
+                    if(xremote_view_press_button(view, button)) model->ok_pressed = true;
                 }
-            }
-            else if (event->type == InputTypeShort &&
-                    event->key == InputKeyBack &&
-                    exit == XRemoteAppExitHold)
-            {
-                button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_PAUSE);
-                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);
-                if (xremote_view_press_button(view, button)) model->back_pressed = true;
-            }
-            else if (event->type == InputTypeRelease)
-            {
-                if (event->key == InputKeyUp) model->up_pressed = false;
-                else if (event->key == InputKeyDown) model->down_pressed = false;
-                else if (event->key == InputKeyLeft) model->left_pressed = false;
-                else if (event->key == InputKeyRight) model->right_pressed = false;
-                else if (event->key == InputKeyOk) model->ok_pressed = false;
-                else if (event->key == InputKeyBack) model->back_pressed = false;
+            } else if(
+                event->type == InputTypeShort && event->key == InputKeyBack &&
+                exit == XRemoteAppExitHold) {
+                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_PLAY);
+                if(xremote_view_press_button(view, button)) model->back_pressed = true;
+            } else if(event->type == InputTypeRelease) {
+                if(event->key == InputKeyUp)
+                    model->up_pressed = false;
+                else if(event->key == InputKeyDown)
+                    model->down_pressed = false;
+                else if(event->key == InputKeyLeft)
+                    model->left_pressed = false;
+                else if(event->key == InputKeyRight)
+                    model->right_pressed = false;
+                else if(event->key == InputKeyOk)
+                    model->ok_pressed = false;
+                else if(event->key == InputKeyBack)
+                    model->back_pressed = false;
             }
         },
         true);
 }
 
-static bool xremote_player_view_input_callback(InputEvent* event, void* context)
-{
+static bool xremote_player_view_input_callback(InputEvent* event, void* context) {
     furi_assert(context);
     XRemoteView* view = (XRemoteView*)context;
     XRemoteAppContext* app_ctx = xremote_view_get_app_context(view);
     XRemoteAppExit exit = app_ctx->app_settings->exit_behavior;
 
-    if (event->key == InputKeyBack &&
-        event->type == InputTypeShort &&
-        exit == XRemoteAppExitPress) return false;
-    else if (event->key == InputKeyBack &&
-        event->type == InputTypeLong &&
-        exit == XRemoteAppExitHold) return false;
+    if(event->key == InputKeyBack && event->type == InputTypeShort && exit == XRemoteAppExitPress)
+        return false;
+    else if(event->key == InputKeyBack && event->type == InputTypeLong && exit == XRemoteAppExitHold)
+        return false;
 
     xremote_player_view_process(view, event);
     return true;
 }
 
-XRemoteView* xremote_player_view_alloc(void* app_ctx)
-{
-    XRemoteView *view = xremote_view_alloc(app_ctx,
-        xremote_player_view_input_callback,
-        xremote_player_view_draw_callback);
+XRemoteView* xremote_player_view_alloc(void* app_ctx) {
+    XRemoteView* view = xremote_view_alloc(
+        app_ctx, xremote_player_view_input_callback, xremote_player_view_draw_callback);
 
     with_view_model(
         xremote_view_get_view(view),
-        XRemoteViewModel* model,
+        XRemoteViewModel * model,
         {
             model->context = app_ctx;
             model->up_pressed = false;
@@ -158,8 +140,7 @@ XRemoteView* xremote_player_view_alloc(void* app_ctx)
             model->back_pressed = false;
             model->ok_pressed = false;
         },
-        true
-    );
+        true);
 
     return view;
 }

+ 187 - 0
views/xremote_signal_view.c

@@ -0,0 +1,187 @@
+/*!
+ *  @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);

+ 21 - 21
xremote.c

@@ -10,48 +10,47 @@
 #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"
 
-void xremote_get_version(char *version, size_t length)
-{
-    snprintf(version, length, "%d.%d.%d",
+void xremote_get_version(char* version, size_t length) {
+    snprintf(
+        version,
+        length,
+        "%d.%d.%d",
         XREMOTE_VERSION_MAJOR,
         XREMOTE_VERSION_MINOR,
         XREMOTE_BUILD_NUMBER);
 }
 
-static uint32_t xremote_view_exit_callback(void* context)
-{
+static uint32_t xremote_view_exit_callback(void* context) {
     UNUSED(context);
     return XRemoteViewSubmenu;
 }
 
-static uint32_t xremote_exit_callback(void* context)
-{
+static uint32_t xremote_exit_callback(void* context) {
     UNUSED(context);
     return VIEW_NONE;
 }
 
-static void xremote_child_clear_callback(void *context)
-{
+static void xremote_child_clear_callback(void* context) {
     xremote_app_assert_void(context);
     xremote_app_free((XRemoteApp*)context);
 }
 
-static XRemoteApp* xremote_about_alloc(XRemoteAppContext* app_ctx)
-{
+static XRemoteApp* xremote_about_alloc(XRemoteAppContext* app_ctx) {
     XRemoteApp* app = xremote_app_alloc(app_ctx);
     xremote_app_view_alloc(app, XRemoteViewAbout, xremote_about_view_alloc);
     xremote_app_view_set_previous_callback(app, xremote_view_exit_callback);
     return app;
 }
 
-void xremote_submenu_callback(void* context, uint32_t index)
-{
+void xremote_submenu_callback(void* context, uint32_t index) {
     furi_assert(context);
     XRemoteApp* app = (XRemoteApp*)context;
 
@@ -60,25 +59,25 @@ void xremote_submenu_callback(void* context, uint32_t index)
     XRemoteApp* child = NULL;
 
     /* Allocate child app and view based on submenu selection */
-    if (index == XRemoteViewLearn)
+    if(index == XRemoteViewLearn)
         child = xremote_learn_alloc(app->app_ctx);
-    else if (index == XRemoteViewIRSubmenu)
+    else if(index == XRemoteViewIRSubmenu)
         child = xremote_control_alloc(app->app_ctx);
-    else if (index == XRemoteViewSettings)
+    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)
+    else if(index == XRemoteViewAbout)
         child = xremote_about_alloc(app->app_ctx);
 
-    if (child != NULL)
-    {
+    if(child != NULL) {
         /* Switch to the view of newely allocated app */
         xremote_app_set_user_context(app, child, xremote_child_clear_callback);
         xremote_app_switch_to_view(child, index);
     }
 }
 
-int32_t xremote_main(void* p)
-{
+int32_t xremote_main(void* p) {
     /* Allocate context and main application */
     XRemoteAppContext* context = xremote_app_context_alloc(p);
     XRemoteApp* app = xremote_app_alloc(context);
@@ -87,6 +86,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);
 

+ 4 - 4
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 2
 
-void xremote_get_version(char *version, size_t length);
+void xremote_get_version(char* version, size_t length);

+ 157 - 0
xremote_analyzer.c

@@ -0,0 +1,157 @@
+/*!
+ *  @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"
+
+struct XRemoteSignalAnalyzer {
+    XRemoteSignalReceiver* ir_receiver;
+    XRemoteClearCallback on_clear;
+    XRemoteAppContext* app_ctx;
+    InfraredSignal* ir_signal;
+    XRemoteView* signal_view;
+    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);

+ 25 - 9
xremote_app.c

@@ -111,6 +111,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;
 }
 
@@ -137,10 +138,22 @@ 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) {
@@ -209,11 +222,17 @@ void xremote_app_submenu_alloc(XRemoteApp* app, uint32_t index, ViewNavigationCa
     app->submenu_id = index;
 
     XRemoteAppSettings* settings = app->app_ctx->app_settings;
+    View* view = submenu_get_view(app->submenu);
+    view_set_previous_callback(view, prev_cb);
+
+#ifdef FW_ORIGIN_Unleashed
     submenu_set_orientation(app->submenu, settings->orientation);
-    view_set_previous_callback(submenu_get_view(app->submenu), prev_cb);
+#else
+    view_set_orientation(view, settings->orientation);
+#endif
 
     ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
-    view_dispatcher_add_view(view_disp, app->submenu_id, submenu_get_view(app->submenu));
+    view_dispatcher_add_view(view_disp, app->submenu_id, view);
 }
 
 void xremote_app_submenu_free(XRemoteApp* app) {
@@ -240,16 +259,13 @@ 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;
     app->context = context;

+ 27 - 22
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,20 +23,21 @@
 #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"
 
-#define XREMOTE_APP_EXTENSION   ".ir"
-#define XREMOTE_APP_FOLDER      ANY_PATH("infrared")
+#define XREMOTE_APP_EXTENSION ".ir"
+#define XREMOTE_APP_FOLDER ANY_PATH("infrared")
+#define XREMOTE_APP_TEXT_MAX 128
 
-#define xremote_app_assert_void(cond) if (!cond) return
-#define xremote_app_assert(cond, var) if (!cond) return var
+#define xremote_app_assert_void(cond) \
+    if(!cond) return
+#define xremote_app_assert(cond, var) \
+    if(!cond) return var
 
-typedef enum {
-    XRemoteAppExitPress,
-    XRemoteAppExitHold
-} XRemoteAppExit;
+typedef enum { XRemoteAppExitPress, XRemoteAppExitHold } XRemoteAppExit;
 
 typedef struct {
     ViewOrientation orientation;
@@ -63,35 +65,38 @@ 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;
     XRemoteView* view_ctx;
     Submenu* submenu;
-    void *context;
+    void* context;
 } XRemoteApp;
 
-void xremote_app_submenu_add(XRemoteApp* app, const char *name, uint32_t index, SubmenuItemCallback callback);
+void xremote_app_submenu_add(
+    XRemoteApp* app,
+    const char* name,
+    uint32_t index,
+    SubmenuItemCallback callback);
 void xremote_app_submenu_alloc(XRemoteApp* app, uint32_t index, ViewNavigationCallback prev_cb);
-void xremote_app_submenu_free(XRemoteApp *app);
+void xremote_app_submenu_free(XRemoteApp* app);
 
-void xremote_app_view_alloc(XRemoteApp *app, uint32_t view_id, XRemoteViewAllocator allocator);
+void xremote_app_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteViewAllocator allocator);
 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);
-void xremote_app_switch_to_view(XRemoteApp *app, uint32_t view_id);
-void xremote_app_switch_to_submenu(XRemoteApp *app);
+bool xremote_app_has_view(XRemoteApp* app, uint32_t view_id);
+void xremote_app_switch_to_view(XRemoteApp* app, uint32_t view_id);
+void xremote_app_switch_to_submenu(XRemoteApp* app);
 
-XRemoteApp* xremote_app_alloc(XRemoteAppContext *ctx);
+XRemoteApp* xremote_app_alloc(XRemoteAppContext* ctx);
 void xremote_app_free(XRemoteApp* app);

+ 23 - 29
xremote_control.c

@@ -15,51 +15,45 @@
 #include "views/xremote_player_view.h"
 #include "views/xremote_custom_view.h"
 
-static uint32_t xremote_control_submenu_exit_callback(void* context)
-{
+static uint32_t xremote_control_submenu_exit_callback(void* context) {
     UNUSED(context);
     return XRemoteViewSubmenu;
 }
 
-static uint32_t xremote_navigation_view_exit_callback(void* context)
-{
+static uint32_t xremote_navigation_view_exit_callback(void* context) {
     UNUSED(context);
     return XRemoteViewIRSubmenu;
 }
 
-static void xremote_ir_clear_callback(void *context)
-{
+static void xremote_ir_clear_callback(void* context) {
     xremote_app_assert_void(context);
     infrared_remote_free((InfraredRemote*)context);
 }
 
-static void xremote_control_submenu_callback(void* context, uint32_t index)
-{
+static void xremote_control_submenu_callback(void* context, uint32_t index) {
     furi_assert(context);
     XRemoteApp* app = context;
 
     /* Allocate new view based on selection */
-    if (index == XRemoteViewIRGeneral)
+    if(index == XRemoteViewIRGeneral)
         xremote_app_view_alloc(app, index, xremote_general_view_alloc);
-    else if (index == XRemoteViewIRControl)
+    else if(index == XRemoteViewIRControl)
         xremote_app_view_alloc(app, index, xremote_control_view_alloc);
-    else if (index == XRemoteViewIRNavigation)
+    else if(index == XRemoteViewIRNavigation)
         xremote_app_view_alloc(app, index, xremote_navigation_view_alloc);
-    else if (index == XRemoteViewIRPlayer)
+    else if(index == XRemoteViewIRPlayer)
         xremote_app_view_alloc(app, index, xremote_player_view_alloc);
-    else if (index == XRemoteViewIRCustom)
+    else if(index == XRemoteViewIRCustom)
         xremote_app_view_alloc(app, index, xremote_custom_view_alloc);
 
-    if (app->view_ctx != NULL)
-    {
+    if(app->view_ctx != NULL) {
         xremote_app_view_set_previous_callback(app, xremote_navigation_view_exit_callback);
         xremote_app_set_view_context(app, app->context, NULL);
         xremote_app_switch_to_view(app, index);
     }
 }
 
-static InfraredRemote* xremote_load_ir_buttons(XRemoteAppContext* app_ctx)
-{
+static InfraredRemote* xremote_load_ir_buttons(XRemoteAppContext* app_ctx) {
     DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
     Storage* storage = furi_record_open(RECORD_STORAGE);
     storage_simply_mkdir(storage, XREMOTE_APP_FOLDER);
@@ -69,15 +63,13 @@ static InfraredRemote* xremote_load_ir_buttons(XRemoteAppContext* app_ctx)
     dialog_file_browser_set_basic_options(&browser, XREMOTE_APP_EXTENSION, &I_IR_Icon_10x10);
     browser.base_path = XREMOTE_APP_FOLDER;
 
-    if (app_ctx->file_path == NULL)
-    {
+    if(app_ctx->file_path == NULL) {
         app_ctx->file_path = furi_string_alloc();
         furi_string_set(app_ctx->file_path, XREMOTE_APP_FOLDER);
     }
 
     /* Show file selection dialog (returns selected file path with variable file_path) */
-    if (!dialog_file_browser_show(dialogs, app_ctx->file_path, app_ctx->file_path, &browser))
-    {
+    if(!dialog_file_browser_show(dialogs, app_ctx->file_path, app_ctx->file_path, &browser)) {
         furi_record_close(RECORD_STORAGE);
         furi_record_close(RECORD_DIALOGS);
         return NULL;
@@ -91,8 +83,7 @@ static InfraredRemote* xremote_load_ir_buttons(XRemoteAppContext* app_ctx)
     furi_record_close(RECORD_STORAGE);
     furi_record_close(RECORD_DIALOGS);
 
-    if (!success)
-    {
+    if(!success) {
         infrared_remote_free(remote);
         return NULL;
     }
@@ -100,8 +91,7 @@ static InfraredRemote* xremote_load_ir_buttons(XRemoteAppContext* app_ctx)
     return remote;
 }
 
-XRemoteApp* xremote_control_alloc(XRemoteAppContext* app_ctx)
-{
+XRemoteApp* xremote_control_alloc(XRemoteAppContext* app_ctx) {
     /* Open file browser and load buttons from selected file */
     InfraredRemote* remote = xremote_load_ir_buttons(app_ctx);
     xremote_app_assert(remote, NULL);
@@ -109,10 +99,14 @@ XRemoteApp* xremote_control_alloc(XRemoteAppContext* app_ctx)
     /* Allocate remote controller app with submenu */
     XRemoteApp* app = xremote_app_alloc(app_ctx);
     xremote_app_submenu_alloc(app, XRemoteViewIRSubmenu, xremote_control_submenu_exit_callback);
-    xremote_app_submenu_add(app, "General", XRemoteViewIRGeneral, xremote_control_submenu_callback);
-    xremote_app_submenu_add(app, "Control", XRemoteViewIRControl, xremote_control_submenu_callback);
-    xremote_app_submenu_add(app, "Navigation", XRemoteViewIRNavigation, xremote_control_submenu_callback);
-    xremote_app_submenu_add(app, "Playback", XRemoteViewIRPlayer, xremote_control_submenu_callback);
+    xremote_app_submenu_add(
+        app, "General", XRemoteViewIRGeneral, xremote_control_submenu_callback);
+    xremote_app_submenu_add(
+        app, "Control", XRemoteViewIRControl, xremote_control_submenu_callback);
+    xremote_app_submenu_add(
+        app, "Navigation", XRemoteViewIRNavigation, xremote_control_submenu_callback);
+    xremote_app_submenu_add(
+        app, "Playback", XRemoteViewIRPlayer, xremote_control_submenu_callback);
     xremote_app_submenu_add(app, "Custom", XRemoteViewIRCustom, xremote_control_submenu_callback);
     xremote_app_set_user_context(app, remote, xremote_ir_clear_callback);
 

+ 384 - 6
xremote_learn.c

@@ -9,16 +9,394 @@
 #include "xremote_learn.h"
 #include "views/xremote_learn_view.h"
 
-static uint32_t xremote_learn_view_exit_callback(void* context)
-{
+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_APP_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);
 }
 
-XRemoteApp* xremote_learn_alloc(XRemoteAppContext* app_ctx)
-{
+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_APP_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_APP_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);

+ 43 - 61
xremote_settings.c

@@ -9,78 +9,65 @@
 #include "xremote_settings.h"
 
 typedef struct {
-    VariableItemList *item_list;
+    VariableItemList* item_list;
     XRemoteAppContext* app_ctx;
 } XRemoteSettingsContext;
 
-#define XREMOTE_ORIENTATION_TEXT_HORIZONTAL     "Horizontal"
-#define XREMOTE_ORIENTATION_TEXT_VERTICAL       "Vertical"
-#define XREMOTE_ORIENTATION_TEXT                "Orientation"
-#define XREMOTE_ORIENTATION_INDEX_HORIZONTAL    0
-#define XREMOTE_ORIENTATION_INDEX_VERTICAL      1
-#define XREMOTE_ORIENTATION_MAX                 2
+#define XREMOTE_ORIENTATION_TEXT_HORIZONTAL "Horizontal"
+#define XREMOTE_ORIENTATION_TEXT_VERTICAL "Vertical"
+#define XREMOTE_ORIENTATION_TEXT "Orientation"
+#define XREMOTE_ORIENTATION_INDEX_HORIZONTAL 0
+#define XREMOTE_ORIENTATION_INDEX_VERTICAL 1
+#define XREMOTE_ORIENTATION_MAX 2
 
-#define XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS        "Press"
-#define XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD         "Hold"
-#define XREMOTE_EXIT_BEHAVIOR_TEXT              "Exit App"
-#define XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS       0
-#define XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD        1
-#define XREMOTE_EXIT_BEHAVIOR_MAX               2
+#define XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS "Press"
+#define XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD "Hold"
+#define XREMOTE_EXIT_BEHAVIOR_TEXT "Exit App"
+#define XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS 0
+#define XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD 1
+#define XREMOTE_EXIT_BEHAVIOR_MAX 2
 
-#define XREMOTE_REPEAT_TEXT                     "IR Msg Repeat"
-#define XREMOTE_REPEAT_MAX                      128
+#define XREMOTE_REPEAT_TEXT "IR Msg Repeat"
+#define XREMOTE_REPEAT_MAX 128
 
-
-static uint32_t xremote_settings_view_exit_callback(void* context)
-{
+static uint32_t xremote_settings_view_exit_callback(void* context) {
     UNUSED(context);
     return XRemoteViewSubmenu;
 }
 
-static uint32_t xremote_settings_get_exit_index(XRemoteAppSettings* settings)
-{
-    return settings->exit_behavior == XRemoteAppExitPress ?
-        XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS :
-        XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD;
+static uint32_t xremote_settings_get_exit_index(XRemoteAppSettings* settings) {
+    return settings->exit_behavior == XRemoteAppExitPress ? XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS :
+                                                            XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD;
 }
 
-static const char* xremote_settings_get_exit_str(XRemoteAppSettings* settings)
-{
-    return settings->exit_behavior == XRemoteAppExitPress ?
-        XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS :
-        XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD;
+static const char* xremote_settings_get_exit_str(XRemoteAppSettings* settings) {
+    return settings->exit_behavior == XRemoteAppExitPress ? XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS :
+                                                            XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD;
 }
 
-static XRemoteAppExit xremote_settings_get_exit_behavior(uint8_t exit_behavior)
-{
-    return exit_behavior == XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS ?
-        XRemoteAppExitPress :
-        XRemoteAppExitHold;
+static XRemoteAppExit xremote_settings_get_exit_behavior(uint8_t exit_behavior) {
+    return exit_behavior == XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS ? XRemoteAppExitPress :
+                                                                XRemoteAppExitHold;
 }
 
-static uint32_t xremote_settings_get_orientation_index(XRemoteAppSettings* settings)
-{
+static uint32_t xremote_settings_get_orientation_index(XRemoteAppSettings* settings) {
     return settings->orientation == ViewOrientationHorizontal ?
-        XREMOTE_ORIENTATION_INDEX_HORIZONTAL :
-        XREMOTE_ORIENTATION_INDEX_VERTICAL;
+               XREMOTE_ORIENTATION_INDEX_HORIZONTAL :
+               XREMOTE_ORIENTATION_INDEX_VERTICAL;
 }
 
-static const char* xremote_settings_get_orientation_str(XRemoteAppSettings* settings)
-{
+static const char* xremote_settings_get_orientation_str(XRemoteAppSettings* settings) {
     return settings->orientation == ViewOrientationHorizontal ?
-        XREMOTE_ORIENTATION_TEXT_HORIZONTAL :
-        XREMOTE_ORIENTATION_TEXT_VERTICAL;
+               XREMOTE_ORIENTATION_TEXT_HORIZONTAL :
+               XREMOTE_ORIENTATION_TEXT_VERTICAL;
 }
 
-static ViewOrientation xremote_settings_get_orientation(uint8_t orientation)
-{
-    return orientation == XREMOTE_ORIENTATION_INDEX_HORIZONTAL ?
-        ViewOrientationHorizontal :
-        ViewOrientationVertical;
+static ViewOrientation xremote_settings_get_orientation(uint8_t orientation) {
+    return orientation == XREMOTE_ORIENTATION_INDEX_HORIZONTAL ? ViewOrientationHorizontal :
+                                                                 ViewOrientationVertical;
 }
 
-static void infrared_settings_orientation_changed(VariableItem* item)
-{
+static void infrared_settings_orientation_changed(VariableItem* item) {
     XRemoteSettingsContext* ctx = variable_item_get_context(item);
     XRemoteAppSettings* settings = ctx->app_ctx->app_settings;
 
@@ -92,8 +79,7 @@ static void infrared_settings_orientation_changed(VariableItem* item)
     xremote_app_settings_store(settings);
 }
 
-static void infrared_settings_repeat_changed(VariableItem* item)
-{
+static void infrared_settings_repeat_changed(VariableItem* item) {
     XRemoteSettingsContext* ctx = variable_item_get_context(item);
     XRemoteAppSettings* settings = ctx->app_ctx->app_settings;
     char repeat_str[8];
@@ -105,8 +91,7 @@ static void infrared_settings_repeat_changed(VariableItem* item)
     xremote_app_settings_store(settings);
 }
 
-static void infrared_settings_exit_changed(VariableItem* item)
-{
+static void infrared_settings_exit_changed(VariableItem* item) {
     XRemoteSettingsContext* ctx = variable_item_get_context(item);
     XRemoteAppSettings* settings = ctx->app_ctx->app_settings;
 
@@ -118,9 +103,8 @@ static void infrared_settings_exit_changed(VariableItem* item)
     xremote_app_settings_store(settings);
 }
 
-static XRemoteSettingsContext* xremote_settings_context_alloc(XRemoteAppContext* app_ctx)
-{
-    XRemoteSettingsContext *context = malloc(sizeof(XRemoteSettingsContext));
+static XRemoteSettingsContext* xremote_settings_context_alloc(XRemoteAppContext* app_ctx) {
+    XRemoteSettingsContext* context = malloc(sizeof(XRemoteSettingsContext));
     XRemoteAppSettings* settings = app_ctx->app_settings;
 
     context->item_list = variable_item_list_alloc();
@@ -128,7 +112,7 @@ static XRemoteSettingsContext* xremote_settings_context_alloc(XRemoteAppContext*
     char repeat_str[8];
 
     /* Configure variable item list view */
-    View *view = variable_item_list_get_view(context->item_list);
+    View* view = variable_item_list_get_view(context->item_list);
     view_set_previous_callback(view, xremote_settings_view_exit_callback);
     view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewSettings, view);
 
@@ -180,16 +164,14 @@ static XRemoteSettingsContext* xremote_settings_context_alloc(XRemoteAppContext*
     return context;
 }
 
-static void xremote_settings_context_clear_callback(void *context)
-{
-    XRemoteSettingsContext *ctx = (XRemoteSettingsContext*)context;
+static void xremote_settings_context_clear_callback(void* context) {
+    XRemoteSettingsContext* ctx = (XRemoteSettingsContext*)context;
     ViewDispatcher* view_disp = ctx->app_ctx->view_dispatcher;
     view_dispatcher_remove_view(view_disp, XRemoteViewSettings);
     variable_item_list_free(ctx->item_list);
 }
 
-XRemoteApp* xremote_settings_alloc(XRemoteAppContext* app_ctx)
-{
+XRemoteApp* xremote_settings_alloc(XRemoteAppContext* app_ctx) {
     XRemoteApp* app = xremote_app_alloc(app_ctx);
     XRemoteSettingsContext* context = xremote_settings_context_alloc(app_ctx);
     xremote_app_set_user_context(app, context, xremote_settings_context_clear_callback);

+ 125 - 0
xremote_signal.c

@@ -0,0 +1,125 @@
+/*!
+ *  @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;
+}

+ 33 - 0
xremote_signal.h

@@ -0,0 +1,33 @@
+/*!
+ *  @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);