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

Add cross_remote from https://github.com/leedave/flipper-zero-cross-remote

git-subtree-dir: cross_remote
git-subtree-mainline: 5dc4104f0c62d0ed026cb10a3d65e54cbbe4b50a
git-subtree-split: 01eb2c1aaa2e6106c60f5a0f941935f04dbeeb4d
David Lee 1 год назад
Родитель
Сommit
3074e48081
87 измененных файлов с 6063 добавлено и 0 удалено
  1. 191 0
      cross_remote/.clang-format
  2. 1 0
      cross_remote/.gitsubtree
  3. 56 0
      cross_remote/README.md
  4. 14 0
      cross_remote/application.fam
  5. 22 0
      cross_remote/docs/README.md
  6. 45 0
      cross_remote/docs/changelog.md
  7. 389 0
      cross_remote/helpers/gui/int_input.c
  8. 72 0
      cross_remote/helpers/gui/int_input.h
  9. 71 0
      cross_remote/helpers/subghz/subghz.c
  10. 9 0
      cross_remote/helpers/subghz/subghz.h
  11. 14 0
      cross_remote/helpers/subghz/subghz_error_type.h
  12. 243 0
      cross_remote/helpers/subghz/subghz_i.c
  13. 85 0
      cross_remote/helpers/subghz/subghz_i.h
  14. 630 0
      cross_remote/helpers/subghz/subghz_txrx.c
  15. 336 0
      cross_remote/helpers/subghz/subghz_txrx.h
  16. 29 0
      cross_remote/helpers/subghz/subghz_txrx_i.h
  17. 77 0
      cross_remote/helpers/subghz/subghz_types.h
  18. 79 0
      cross_remote/helpers/xremote_custom_event.h
  19. 35 0
      cross_remote/helpers/xremote_haptic.c
  20. 9 0
      cross_remote/helpers/xremote_haptic.h
  21. 38 0
      cross_remote/helpers/xremote_led.c
  22. 7 0
      cross_remote/helpers/xremote_led.h
  23. 25 0
      cross_remote/helpers/xremote_speaker.c
  24. 8 0
      cross_remote/helpers/xremote_speaker.h
  25. 122 0
      cross_remote/helpers/xremote_storage.c
  26. 18 0
      cross_remote/helpers/xremote_storage.h
  27. BIN
      cross_remote/icons/ButtonDown_10x5.png
  28. BIN
      cross_remote/icons/ButtonUp_10x5.png
  29. BIN
      cross_remote/icons/KeyBackspaceSelected_16x9.png
  30. BIN
      cross_remote/icons/KeyBackspace_16x9.png
  31. BIN
      cross_remote/icons/KeySaveSelected_24x11.png
  32. BIN
      cross_remote/icons/KeySave_24x11.png
  33. BIN
      cross_remote/icons/ir_10px.png
  34. BIN
      cross_remote/icons/ir_transmit_128x64.png
  35. BIN
      cross_remote/icons/pause_128x64.png
  36. BIN
      cross_remote/icons/sg_10px.png
  37. BIN
      cross_remote/icons/sg_transmit_128x64.png
  38. BIN
      cross_remote/icons/xr_10px.png
  39. BIN
      cross_remote/icons/xremote_10px.png
  40. 276 0
      cross_remote/models/cross/xremote_cross_remote.c
  41. 29 0
      cross_remote/models/cross/xremote_cross_remote.h
  42. 290 0
      cross_remote/models/cross/xremote_cross_remote_item.c
  43. 37 0
      cross_remote/models/cross/xremote_cross_remote_item.h
  44. 91 0
      cross_remote/models/infrared/xremote_ir_remote.c
  45. 22 0
      cross_remote/models/infrared/xremote_ir_remote.h
  46. 33 0
      cross_remote/models/infrared/xremote_ir_remote_button.c
  47. 13 0
      cross_remote/models/infrared/xremote_ir_remote_button.h
  48. 218 0
      cross_remote/models/infrared/xremote_ir_signal.c
  49. 38 0
      cross_remote/models/infrared/xremote_ir_signal.h
  50. 65 0
      cross_remote/models/subghz/xremote_sg_remote.c
  51. 22 0
      cross_remote/models/subghz/xremote_sg_remote.h
  52. 30 0
      cross_remote/scenes/xremote_scene.c
  53. 29 0
      cross_remote/scenes/xremote_scene.h
  54. 18 0
      cross_remote/scenes/xremote_scene_config.h
  55. 103 0
      cross_remote/scenes/xremote_scene_create.c
  56. 98 0
      cross_remote/scenes/xremote_scene_create_add.c
  57. 64 0
      cross_remote/scenes/xremote_scene_edit_item.c
  58. 55 0
      cross_remote/scenes/xremote_scene_infoscreen.c
  59. 42 0
      cross_remote/scenes/xremote_scene_ir_list.c
  60. 81 0
      cross_remote/scenes/xremote_scene_ir_remote.c
  61. 58 0
      cross_remote/scenes/xremote_scene_ir_timer.c
  62. 87 0
      cross_remote/scenes/xremote_scene_menu.c
  63. 54 0
      cross_remote/scenes/xremote_scene_pause_set.c
  64. 74 0
      cross_remote/scenes/xremote_scene_save_remote.c
  65. 45 0
      cross_remote/scenes/xremote_scene_save_remote_item.c
  66. 163 0
      cross_remote/scenes/xremote_scene_settings.c
  67. 43 0
      cross_remote/scenes/xremote_scene_sg_list.c
  68. 184 0
      cross_remote/scenes/xremote_scene_transmit.c
  69. 38 0
      cross_remote/scenes/xremote_scene_wip.c
  70. 47 0
      cross_remote/scenes/xremote_scene_xr_list.c
  71. 42 0
      cross_remote/scenes/xremote_scene_xr_list_edit.c
  72. 65 0
      cross_remote/scenes/xremote_scene_xr_list_edit_item.c
  73. BIN
      cross_remote/screenshots/xremote_1.png
  74. BIN
      cross_remote/screenshots/xremote_2.png
  75. BIN
      cross_remote/screenshots/xremote_3.png
  76. BIN
      cross_remote/screenshots/xremote_4.png
  77. BIN
      cross_remote/screenshots/xremote_5.png
  78. BIN
      cross_remote/screenshots/xremote_6.png
  79. 112 0
      cross_remote/views/xremote_infoscreen.c
  80. 20 0
      cross_remote/views/xremote_infoscreen.h
  81. 138 0
      cross_remote/views/xremote_pause_set.c
  82. 24 0
      cross_remote/views/xremote_pause_set.h
  83. 142 0
      cross_remote/views/xremote_transmit.c
  84. 24 0
      cross_remote/views/xremote_transmit.h
  85. 214 0
      cross_remote/xremote.c
  86. 95 0
      cross_remote/xremote.h
  87. 115 0
      cross_remote/xremote_i.h

+ 191 - 0
cross_remote/.clang-format

@@ -0,0 +1,191 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle:    ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires:  false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Left
+PPIndentWidth:   -1
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard:        c++03
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

+ 1 - 0
cross_remote/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/leedave/flipper-zero-cross-remote main /

+ 56 - 0
cross_remote/README.md

@@ -0,0 +1,56 @@
+# Flipper Zero Cross Remote
+
+## Current State
+- Infrared working
+- SubGhz working
+- Pause working
+- IR Timing features working
+
+## What this is?
+This app combines commands used in IR and SubGhz into playlists that can be run with one click
+<br><br>
+
+### What good is this?
+Imagine you want to sit down and watch a movie after a long days work. <br>
+Your probably do something like the following<br>
+- Turn on your TV with the IR TV remote<br>
+- Turn on your Bluray player with the Blueray IR remote<br>
+- Turn on your surround sound with the speaker IR remote<br>
+- Turn on your ceiling fan using a subGhz remote<br>
+- Turn on your AC using another remote<br>
+- etc<br>
+<br>
+Wouldn't it be nicer to simply click one button and let everything happen? This is what this app wants to do. <br>
+
+### Features
+- Select commands from saved subGhz transmissions
+- Select commands registered with the IR App (choose from the created remote buttons)<br>
+- Chain these commands using an easy UI<br>
+- Save chained commands to a file<br>
+- Add pauses, becaue target systems are not always fast enough for multiple commands<br>
+- Run file containing chained IR & SubGhz commands<br>
+
+### Settings
+- LED FX, allow the LED to blink
+- Save settings, stores a file with your settings in it on exit
+- IR time ms, the default duration of an IR signal transmission. Individual times can be set
+- SubG. time ms, the default duration of a SubGhz signal. Only needed for Encoded signals, RAW files play until finished
+
+### Limitations
+SubGhz commands will stop working if you move/rename/delete the original files on your Flipper. This is because of how the Flippers SubGhz worker expects data. 
+
+After an upgrade a crash on exit can occur, due to small improvements to the file formats. Sorry about that, it only happens once. 
+
+## How to install on Flipper Zero
+- If you do not have one, download a firmware<br>
+- Plug your Flipper Zero in via USB. <br>
+- Copy the contents of this folder into the applications_user folder of your firmware. <br> 
+
+Then run the command: 
+ ```
+.\fbt launch_app APPSRC=applications_user/xremote
+ ```
+The application will be compiled and copied onto your device. 
+
+## Licensing
+This code is open-source and may be used for whatever you want to do with it. 

+ 14 - 0
cross_remote/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="xremote",
+    name="Cross Remote",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="xremote_app",
+    stack_size=3 * 1024,
+    fap_icon="icons/xremote_10px.png",
+    fap_icon_assets="icons",
+    fap_version="2.5",
+    fap_category="Infrared",
+    fap_author="Leedave",
+    fap_description="One-Click, sends multiple commands",
+    fap_weburl="https://github.com/leedave/flipper-zero-cross-remote",
+)

+ 22 - 0
cross_remote/docs/README.md

@@ -0,0 +1,22 @@
+## Cross Remote
+
+This app combines your IR and SubGhz commands into a playlist that can be run with one click. Pauses can be set inbetween, as IR/SubGhz devices typically are not so fast. 
+
+## Features
+- Read out commands you recorded in the IR app
+- Read out commands you saved as .sub files
+- Combine commands to a chain/playlist 
+- Add pauses inbetween commands 
+- Save your command chain / playlist to flipper
+- Disable LED effects if not wanted
+- Configure duration of IR Signals
+- Configure default duration of Encoded SubGhz Signals
+
+## What good is this?
+
+Example what you command chain (playlist) could do with one click
+- Turn on your TV with the IR TV remote
+- Turn on your Bluray player with the Blueray IR remote
+- Turn on your surround sound with the speaker IR remote
+- Turn on your ceiling fan using a subGhz remote
+- Turn on your AC using another remote

+ 45 - 0
cross_remote/docs/changelog.md

@@ -0,0 +1,45 @@
+## 2.5
+- Back button on info screen now reacts the same as ok button
+- Fixed crash on exit in fw 0.100.3
+
+## 2.4
+- Added support for encoded SubGhz files
+- Added timer for SubGhz transmission (not needed in RAW files)
+- Added version number to app info screen
+- Minor updates/instrutions in readme.md
+
+## 2.3
+- Fixed Crash after creating chains with SubGhz Items
+
+## 2.2
+- Fixed incompatibility to Flipper-catalog / uFBT compiler
+
+## v2.1
+- Added ability to individually set IR Signal time
+
+## v2.0
+- SubGhz Functionality added
+- Slight change in Storage format to enalbe individual IR timings later (feature request)
+- Fixed more memory leaks
+- Fixed error where Flipper crashes after deleting a command chain
+- Updated Manifest for better CFW support
+- Updated readme for new inputs
+
+## v1.1
+- Patched memory leak in storage
+
+## v1.0
+- Added option to change IR Signal Timing
+
+## v0.9
+
+- New Repository for App 
+- Removed Loading animation for compatibility to API 0.95.0
+- Fixed issue with redundant header
+
+## v0.8
+
+First release to Application Catalog
+- IR Feature Working
+- Pause Features Working 
+- Added warning for block missing SubGhz Features

+ 389 - 0
cross_remote/helpers/gui/int_input.c

@@ -0,0 +1,389 @@
+#include "int_input.h"
+
+#include <gui/elements.h>
+#include <furi.h>
+
+/** IntInput type */
+struct IntInput {
+    View* view;
+};
+
+typedef struct {
+    const char text;
+    const uint8_t x;
+    const uint8_t y;
+} IntInputKey;
+
+typedef struct {
+    const char* header;
+    char* text_buffer;
+    size_t text_buffer_size;
+    bool clear_default_text;
+
+    IntInputCallback callback;
+    void* callback_context;
+
+    int8_t selected_row;
+    uint8_t selected_column;
+} IntInputModel;
+
+static const uint8_t keyboard_origin_x = 7;
+static const uint8_t keyboard_origin_y = 31;
+static const uint8_t keyboard_row_count = 2;
+static const uint8_t enter_symbol = '\r';
+static const uint8_t backspace_symbol = '\b';
+
+static const IntInputKey keyboard_keys_row_1[] = {
+    {'0', 0, 12},
+    {'1', 11, 12},
+    {'2', 22, 12},
+    {'3', 33, 12},
+    {'4', 44, 12},
+    {backspace_symbol, 103, 4},
+};
+
+static const IntInputKey keyboard_keys_row_2[] = {
+    {'5', 0, 26},
+    {'6', 11, 26},
+    {'7', 22, 26},
+    {'8', 33, 26},
+    {'9', 44, 26},
+    {enter_symbol, 95, 17},
+};
+
+/** Get row size
+ *
+ * @param      row_index  Index of row
+ *
+ * @return     uint8_t Row size
+ */
+static uint8_t int_input_get_row_size(uint8_t row_index) {
+    uint8_t row_size = 0;
+
+    switch(row_index + 1) {
+    case 1:
+        row_size = COUNT_OF(keyboard_keys_row_1);
+        break;
+    case 2:
+        row_size = COUNT_OF(keyboard_keys_row_2);
+        break;
+    default:
+        furi_crash();
+    }
+
+    return row_size;
+}
+
+/** Get row pointer
+ *
+ * @param      row_index  Index of row
+ *
+ * @return     const IntInputKey* Row pointer
+ */
+static const IntInputKey* int_input_get_row(uint8_t row_index) {
+    const IntInputKey* row = NULL;
+
+    switch(row_index + 1) {
+    case 1:
+        row = keyboard_keys_row_1;
+        break;
+    case 2:
+        row = keyboard_keys_row_2;
+        break;
+    default:
+        furi_crash();
+    }
+
+    return row;
+}
+
+/** Draw input box (common view)
+ *
+ * @param      canvas  The canvas
+ * @param      model   The model
+ */
+static void int_input_draw_input(Canvas* canvas, IntInputModel* model) {
+    const uint8_t text_x = 8;
+    const uint8_t text_y = 25;
+
+    elements_slightly_rounded_frame(canvas, 6, 14, 116, 15);
+
+    const char* text = model->text_buffer;
+    canvas_draw_str(canvas, text_x, text_y, text);
+}
+
+static void int_input_backspace_cb(IntInputModel* model) {
+    uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
+    if(text_length > 0) {
+        model->text_buffer[text_length - 1] = 0;
+    }
+}
+
+/** Handle up button
+ *
+ * @param      model  The model
+ */
+static void int_input_handle_up(IntInputModel* model) {
+    if(model->selected_row > 0) {
+        model->selected_row--;
+    }
+}
+
+/** Handle down button
+ *
+ * @param      model  The model
+ */
+static void int_input_handle_down(IntInputModel* model) {
+    if(model->selected_row < keyboard_row_count - 1) {
+        model->selected_row += 1;
+    }
+}
+
+/** Handle left button
+ *
+ * @param      model  The model
+ */
+static void int_input_handle_left(IntInputModel* model) {
+    if(model->selected_column > 0) {
+        model->selected_column--;
+    } else {
+        model->selected_column = int_input_get_row_size(model->selected_row) - 1;
+    }
+}
+
+/** Handle right button
+ *
+ * @param      model  The model
+ */
+static void int_input_handle_right(IntInputModel* model) {
+    if(model->selected_column < int_input_get_row_size(model->selected_row) - 1) {
+        model->selected_column++;
+    } else {
+        model->selected_column = 0;
+    }
+}
+
+/** Handle OK button
+ *
+ * @param      model  The model
+ */
+static void int_input_handle_ok(IntInputModel* model) {
+    char selected = int_input_get_row(model->selected_row)[model->selected_column].text;
+    size_t text_length = strlen(model->text_buffer);
+    if(selected == enter_symbol) {
+        model->callback(model->callback_context);
+    } else if(selected == backspace_symbol) {
+        int_input_backspace_cb(model);
+    } else {
+        if(model->clear_default_text) {
+            text_length = 0;
+        }
+        if(text_length < (model->text_buffer_size - 1)) {
+            model->text_buffer[text_length] = selected;
+            model->text_buffer[text_length + 1] = 0;
+        }
+    }
+    model->clear_default_text = false;
+}
+
+/** Draw callback
+ *
+ * @param      canvas  The canvas
+ * @param      _model  The model
+ */
+static void int_input_view_draw_callback(Canvas* canvas, void* _model) {
+    IntInputModel* model = _model;
+    uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
+    UNUSED(text_length);
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    int_input_draw_input(canvas, model);
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 9, model->header);
+    canvas_set_font(canvas, FontKeyboard);
+    // Draw keyboard
+    for(uint8_t row = 0; row < keyboard_row_count; row++) {
+        const uint8_t column_count = int_input_get_row_size(row);
+        const IntInputKey* keys = int_input_get_row(row);
+
+        for(size_t column = 0; column < column_count; column++) {
+            if(keys[column].text == enter_symbol) {
+                canvas_set_color(canvas, ColorBlack);
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySaveSelected_24x11);
+                } else {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySave_24x11);
+                }
+            } else if(keys[column].text == backspace_symbol) {
+                canvas_set_color(canvas, ColorBlack);
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspaceSelected_16x9);
+                } else {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspace_16x9);
+                }
+            } else {
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_set_color(canvas, ColorBlack);
+                    canvas_draw_box(
+                        canvas,
+                        keyboard_origin_x + keys[column].x - 3,
+                        keyboard_origin_y + keys[column].y - 10,
+                        11,
+                        13);
+                    canvas_set_color(canvas, ColorWhite);
+                } else if(model->selected_row == -1 && row == 0 && model->selected_column == column) {
+                    canvas_set_color(canvas, ColorBlack);
+                    canvas_draw_frame(
+                        canvas,
+                        keyboard_origin_x + keys[column].x - 3,
+                        keyboard_origin_y + keys[column].y - 10,
+                        11,
+                        13);
+                } else {
+                    canvas_set_color(canvas, ColorBlack);
+                }
+
+                canvas_draw_glyph(
+                    canvas,
+                    keyboard_origin_x + keys[column].x,
+                    keyboard_origin_y + keys[column].y,
+                    keys[column].text);
+            }
+        }
+    }
+}
+
+/** Input callback
+ *
+ * @param      event    The event
+ * @param      context  The context
+ *
+ * @return     true
+ * @return     false
+ */
+static bool int_input_view_input_callback(InputEvent* event, void* context) {
+    IntInput* int_input = context;
+    furi_assert(int_input);
+
+    bool consumed = false;
+
+    // Fetch the model
+    IntInputModel* model = view_get_model(int_input->view);
+
+    if(event->type == InputTypeShort || event->type == InputTypeLong ||
+       event->type == InputTypeRepeat) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyLeft:
+            int_input_handle_left(model);
+            break;
+        case InputKeyRight:
+            int_input_handle_right(model);
+            break;
+        case InputKeyUp:
+            int_input_handle_up(model);
+            break;
+        case InputKeyDown:
+            int_input_handle_down(model);
+            break;
+        case InputKeyOk:
+            int_input_handle_ok(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+
+    // commit view
+    view_commit_model(int_input->view, consumed);
+
+    return consumed;
+}
+
+void int_input_reset(IntInput* int_input) {
+    FURI_LOG_D("INT_INPUT", "Resetting Model");
+    furi_assert(int_input);
+    with_view_model(
+        int_input->view,
+        IntInputModel * model,
+        {
+            model->header = "";
+            model->selected_row = 0;
+            model->selected_column = 0;
+            model->clear_default_text = false;
+            model->text_buffer = "";
+            model->text_buffer_size = 0;
+            model->callback = NULL;
+            model->callback_context = NULL;
+        },
+        true);
+}
+
+IntInput* int_input_alloc() {
+    IntInput* int_input = malloc(sizeof(IntInput));
+    int_input->view = view_alloc();
+    view_set_context(int_input->view, int_input);
+    view_allocate_model(int_input->view, ViewModelTypeLocking, sizeof(IntInputModel));
+    view_set_draw_callback(int_input->view, int_input_view_draw_callback);
+    view_set_input_callback(int_input->view, int_input_view_input_callback);
+
+    int_input_reset(int_input);
+
+    return int_input;
+}
+
+void int_input_free(IntInput* int_input) {
+    furi_assert(int_input);
+    view_free(int_input->view);
+    free(int_input);
+}
+
+View* int_input_get_view(IntInput* int_input) {
+    furi_assert(int_input);
+    return int_input->view;
+}
+
+void int_input_set_result_callback(
+    IntInput* int_input,
+    IntInputCallback callback,
+    void* callback_context,
+    char* text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text) {
+    with_view_model(
+        int_input->view,
+        IntInputModel * model,
+        {
+            model->callback = callback;
+            model->callback_context = callback_context;
+            model->text_buffer = text_buffer;
+            model->text_buffer_size = text_buffer_size;
+            model->clear_default_text = clear_default_text;
+        },
+        true);
+}
+
+void int_input_set_header_text(IntInput* int_input, const char* text) {
+    with_view_model(
+        int_input->view, IntInputModel * model, { model->header = text; }, true);
+}

+ 72 - 0
cross_remote/helpers/gui/int_input.h

@@ -0,0 +1,72 @@
+/**
+ * @file int_input.h
+ * GUI: Integer string keyboard view module API
+ */
+
+#pragma once
+
+#include <gui/view.h>
+#include "xremote_icons.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Int input anonymous structure  */
+typedef struct IntInput IntInput;
+
+/** callback that is executed on save button press */
+typedef void (*IntInputCallback)(void* context);
+
+/** callback that is executed when byte buffer is changed */
+typedef void (*IntChangedCallback)(void* context);
+
+/** Allocate and initialize Int input. This Int input is used to enter Ints.
+ *
+ * @return     IntInput instance pointer
+ */
+IntInput* int_input_alloc();
+
+/** Deinitialize and free byte input
+ *
+ * @param      int_input  Int input instance
+ */
+void int_input_free(IntInput* int_input);
+
+/** Get byte input view
+ *
+ * @param      int_input  byte input instance
+ *
+ * @return     View instance that can be used for embedding
+ */
+View* int_input_get_view(IntInput* int_input);
+
+/** Set byte input result callback
+ *
+ * @param      int_input          byte input instance
+ * @param      input_callback     input callback fn
+ * @param      changed_callback   changed callback fn
+ * @param      callback_context   callback context
+ * @param      text_buffer        buffer to use
+ * @param      text_buffer_size   buffer length
+ * @param      clear_default_text clear previous entry
+ */
+
+void int_input_set_result_callback(
+    IntInput* int_input,
+    IntInputCallback input_callback,
+    void* callback_context,
+    char* text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text);
+
+/** Set byte input header text
+ *
+ * @param      int_input  byte input instance
+ * @param      text        text to be shown
+ */
+void int_input_set_header_text(IntInput* int_input, const char* text);
+
+#ifdef __cplusplus
+}
+#endif

+ 71 - 0
cross_remote/helpers/subghz/subghz.c

@@ -0,0 +1,71 @@
+/* Reduced variant of the Flipper Zero SubGhz Class */
+
+#include "subghz_i.h"
+#include "../../helpers/xremote_custom_event.h"
+#include "../../helpers/xremote_led.h"
+
+SubGhz* subghz_alloc() {
+    SubGhz* subghz = malloc(sizeof(SubGhz));
+
+    subghz->file_path = furi_string_alloc();
+    subghz->txrx = subghz_txrx_alloc();
+    subghz->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    return subghz;
+}
+
+void subghz_free(SubGhz* subghz) {
+    subghz_txrx_free(subghz->txrx);
+    furi_string_free(subghz->file_path);
+    furi_record_close(RECORD_DIALOGS);
+
+    // The rest
+    free(subghz);
+}
+
+void subghz_scene_transmit_callback_end_tx(void* context) {
+    furi_assert(context);
+    FURI_LOG_D(TAG, "callback end");
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, XRemoteCustomEventViewTransmitterSendStop);
+
+    //app->state_notifications = SubGhzNotificationStateIDLE;
+    //subghz_txrx_stop(app->subghz->txrx);
+    //app->transmitting = false;
+    //xremote_scene_ir_notification_message(app, SubGhzNotificationMessageBlinkStop);
+    xremote_cross_remote_set_transmitting(app->cross_remote, XRemoteTransmittingStopSubghz);
+}
+
+void subghz_send(void* context, const char* path) {
+    XRemote* app = context;
+
+    if(!subghz_load_protocol_from_file(app->subghz, path)) {
+        xremote_cross_remote_set_transmitting(app->cross_remote, XRemoteTransmittingStop);
+        app->transmitting = false;
+        return;
+    }
+
+    if(subghz_get_load_type_file(app->subghz) == SubGhzLoadTypeFileRaw) {
+        FURI_LOG_D(TAG, "Starting Transmission");
+        subghz_txrx_tx_start(
+            app->subghz->txrx,
+            subghz_txrx_get_fff_data(app->subghz->txrx)); //Seems like it must be done this way
+
+        FURI_LOG_D(TAG, "setting sugbhz raw file encoder worker callback");
+        subghz_txrx_set_raw_file_encoder_worker_callback_end(
+            app->subghz->txrx, subghz_scene_transmit_callback_end_tx, app);
+        app->state_notifications = SubGhzNotificationStateTx;
+
+        FURI_LOG_D(TAG, "Finished Transmitting");
+    } else {
+        subghz_tx_start(app->subghz, subghz_txrx_get_fff_data(app->subghz->txrx));
+        app->state_notifications = SubGhzNotificationStateTx;
+        furi_thread_flags_wait(0, FuriFlagWaitAny, app->sg_timing);
+        app->state_notifications = SubGhzNotificationStateIDLE;
+        subghz_txrx_stop(app->subghz->txrx);
+
+        xremote_cross_remote_set_transmitting(app->cross_remote, XRemoteTransmittingStop);
+        app->transmitting = false;
+    }
+}

+ 9 - 0
cross_remote/helpers/subghz/subghz.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "subghz_i.h"
+
+typedef struct SubGhz SubGhz;
+
+SubGhz* subghz_alloc();
+void subghz_free(SubGhz* subghz);
+void subghz_send(void* context, const char* path);

+ 14 - 0
cross_remote/helpers/subghz/subghz_error_type.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+/** SubGhzErrorType */
+typedef enum {
+    SubGhzErrorTypeNoError = 0, /** There are no errors */
+    SubGhzErrorTypeParseFile =
+        1, /** File parsing error, or wrong file structure, or missing required parameters. more accurate data can be obtained through the debug port */
+    SubGhzErrorTypeOnlyRX =
+        2, /** Transmission on this frequency is blocked by regional settings */
+    SubGhzErrorTypeParserOthers = 3, /** Error in protocol parameters description */
+} SubGhzErrorType;

+ 243 - 0
cross_remote/helpers/subghz/subghz_i.c

@@ -0,0 +1,243 @@
+#include "subghz_i.h"
+
+#include "subghz/types.h"
+#include <math.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+#include <flipper_format/flipper_format.h>
+
+#include <flipper_format/flipper_format_i.h>
+#include <lib/toolbox/stream/stream.h>
+#include <lib/subghz/protocols/raw.h>
+
+//#define TAG "SubGhz"
+/*
+void subghz_set_default_preset(SubGhz* subghz) {
+    furi_assert(subghz);
+    subghz_txrx_set_preset(
+        subghz->txrx,
+        "AM650",
+        subghz_setting_get_default_frequency(subghz_txrx_get_setting(subghz->txrx)),
+        NULL,
+        0);
+}*/
+
+bool subghz_tx_start(SubGhz* subghz, FlipperFormat* flipper_format) {
+    switch(subghz_txrx_tx_start(subghz->txrx, flipper_format)) {
+    case SubGhzTxRxStartTxStateErrorParserOthers:
+        dialog_message_show_storage_error(
+            subghz->dialogs, "Error in protocol\nparameters\ndescription");
+        break;
+    case SubGhzTxRxStartTxStateErrorOnlyRx:
+        // FURI_LOG_D(TAG, 'Cannot send, only RX possible');
+        break;
+
+    default:
+        return true;
+        break;
+    }
+    return false;
+}
+
+bool subghz_key_load(SubGhz* subghz, const char* file_path) { //, bool show_dialog) {
+    furi_assert(subghz);
+    furi_assert(file_path);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
+    Stream* fff_data_stream =
+        flipper_format_get_raw_stream(subghz_txrx_get_fff_data(subghz->txrx));
+
+    SubGhzLoadKeyState load_key_state = SubGhzLoadKeyStateParseErr;
+    FuriString* temp_str = furi_string_alloc();
+    uint32_t temp_data32;
+
+    do {
+        stream_clean(fff_data_stream);
+        if(!flipper_format_file_open_existing(fff_data_file, file_path)) {
+            FURI_LOG_E(TAG, "Error open file %s", file_path);
+            break;
+        }
+
+        if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) {
+            FURI_LOG_E(TAG, "Missing or incorrect header");
+            break;
+        }
+
+        if(((!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_KEY_FILE_TYPE)) ||
+            (!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_RAW_FILE_TYPE))) &&
+           temp_data32 == SUBGHZ_KEY_FILE_VERSION) {
+        } else {
+            FURI_LOG_E(TAG, "Type or version mismatch");
+            break;
+        }
+
+        //Load frequency
+        if(!flipper_format_read_uint32(fff_data_file, "Frequency", &temp_data32, 1)) {
+            FURI_LOG_E(TAG, "Missing Frequency");
+            break;
+        }
+
+        if(!subghz_txrx_radio_device_is_frequecy_valid(subghz->txrx, temp_data32)) {
+            FURI_LOG_E(TAG, "Frequency not supported");
+            break;
+        }
+
+        //Load preset
+        if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) {
+            FURI_LOG_E(TAG, "Missing Preset");
+            break;
+        }
+
+        furi_string_set_str(
+            temp_str, subghz_txrx_get_preset_name(subghz->txrx, furi_string_get_cstr(temp_str)));
+        if(!strcmp(furi_string_get_cstr(temp_str), "")) {
+            break;
+        }
+        SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
+
+        if(!strcmp(furi_string_get_cstr(temp_str), "CUSTOM")) {
+            //TODO FL-3551: add Custom_preset_module
+            //delete preset if it already exists
+            subghz_setting_delete_custom_preset(setting, furi_string_get_cstr(temp_str));
+            //load custom preset from file
+            if(!subghz_setting_load_custom_preset(
+                   setting, furi_string_get_cstr(temp_str), fff_data_file)) {
+                FURI_LOG_E(TAG, "Missing Custom preset");
+                break;
+            }
+        }
+        size_t preset_index =
+            subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(temp_str));
+        subghz_txrx_set_preset(
+            subghz->txrx,
+            furi_string_get_cstr(temp_str),
+            temp_data32,
+            subghz_setting_get_preset_data(setting, preset_index),
+            subghz_setting_get_preset_data_size(setting, preset_index));
+
+        //Load protocol
+        if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
+            FURI_LOG_E(TAG, "Missing Protocol");
+            break;
+        }
+
+        FlipperFormat* fff_data = subghz_txrx_get_fff_data(subghz->txrx);
+        if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) {
+            //if RAW
+            subghz->load_type_file = SubGhzLoadTypeFileRaw;
+            subghz_protocol_raw_gen_fff_data(
+                fff_data, file_path, subghz_txrx_radio_device_get_name(subghz->txrx));
+        } else {
+            subghz->load_type_file = SubGhzLoadTypeFileKey;
+            stream_copy_full(
+                flipper_format_get_raw_stream(fff_data_file),
+                flipper_format_get_raw_stream(fff_data));
+        }
+
+        if(subghz_txrx_load_decoder_by_name_protocol(
+               subghz->txrx, furi_string_get_cstr(temp_str))) {
+            SubGhzProtocolStatus status = subghz_protocol_decoder_base_deserialize(
+                subghz_txrx_get_decoder(subghz->txrx), fff_data);
+            if(status != SubGhzProtocolStatusOk) {
+                load_key_state = SubGhzLoadKeyStateProtocolDescriptionErr;
+                break;
+            }
+        } else {
+            FURI_LOG_E(TAG, "Protocol not found");
+            break;
+        }
+
+        load_key_state = SubGhzLoadKeyStateOK;
+    } while(0);
+
+    furi_string_free(temp_str);
+    flipper_format_free(fff_data_file);
+    furi_record_close(RECORD_STORAGE);
+
+    switch(load_key_state) {
+    case SubGhzLoadKeyStateParseErr:
+        //if(show_dialog) {
+        //    dialog_message_show_storage_error(subghz->dialogs, "Cannot parse\nfile");
+        //}
+        return false;
+    case SubGhzLoadKeyStateProtocolDescriptionErr:
+        //if(show_dialog) {
+        //    dialog_message_show_storage_error(
+        //        subghz->dialogs, "Error in protocol\nparameters\ndescription");
+        //}
+        return false;
+
+    case SubGhzLoadKeyStateOK:
+        return true;
+
+    default:
+        furi_crash("SubGhz: Unknown load_key_state.");
+        return false;
+    }
+    return false;
+}
+
+SubGhzLoadTypeFile subghz_get_load_type_file(SubGhz* subghz) {
+    furi_assert(subghz);
+    return subghz->load_type_file;
+}
+
+bool subghz_load_protocol_from_file(SubGhz* subghz, const char* path) {
+    furi_assert(subghz);
+
+    bool res = false;
+
+    res = subghz_key_load(subghz, path); //, true);
+    return res;
+}
+
+/*bool subghz_file_available(SubGhz* subghz) {
+    furi_assert(subghz);
+    bool ret = true;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    FS_Error fs_result =
+        storage_common_stat(storage, furi_string_get_cstr(subghz->file_path), NULL);
+
+    if(fs_result != FSE_OK) {
+        dialog_message_show_storage_error(subghz->dialogs, "File not available\n file/directory");
+        ret = false;
+    }
+
+    furi_record_close(RECORD_STORAGE);
+    return ret;
+}*/
+
+/*bool subghz_path_is_file(FuriString* path) {
+    return furi_string_end_with(path, SUBGHZ_APP_FILENAME_EXTENSION);
+}*/
+
+/*void subghz_lock(SubGhz* subghz) {
+    furi_assert(subghz);
+    subghz->lock = SubGhzLockOn;
+}*/
+
+/*void subghz_unlock(SubGhz* subghz) {
+    furi_assert(subghz);
+    subghz->lock = SubGhzLockOff;
+}*/
+
+/*bool subghz_is_locked(SubGhz* subghz) {
+    furi_assert(subghz);
+    return (subghz->lock == SubGhzLockOn);
+}*/
+
+/*void subghz_rx_key_state_set(SubGhz* subghz, SubGhzRxKeyState state) {
+    furi_assert(subghz);
+    subghz->rx_key_state = state;
+}*/
+
+/*SubGhzRxKeyState subghz_rx_key_state_get(SubGhz* subghz) {
+    furi_assert(subghz);
+    return subghz->rx_key_state;
+}*/

+ 85 - 0
cross_remote/helpers/subghz/subghz_i.h

@@ -0,0 +1,85 @@
+#pragma once
+
+#include "subghz_types.h"
+#include "subghz_error_type.h"
+#include <lib/subghz/types.h>
+#include "subghz.h"
+#include "../xremote_storage.h"
+
+/*#include "views/receiver.h"
+#include "views/transmitter.h"
+#include "views/subghz_frequency_analyzer.h"
+#include "views/subghz_read_raw.h"
+
+#include <gui/gui.h>
+#include <dialogs/dialogs.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/widget.h>
+
+#include <subghz/scenes/subghz_scene.h>
+
+#include "subghz_history.h"
+
+#include <gui/modules/variable_item_list.h>
+#include <lib/toolbox/path.h>
+
+#include "rpc/rpc_app.h"
+
+#include "helpers/subghz_threshold_rssi.h"
+
+
+*/
+#include "subghz_txrx.h"
+
+#define SUBGHZ_MAX_LEN_NAME 64
+
+typedef struct SubGhz SubGhz;
+
+struct SubGhz {
+    SubGhzTxRx* txrx;
+    FuriString* file_path;
+    DialogsApp* dialogs;
+    //FuriString* file_path_tmp;
+    //char file_name_tmp[SUBGHZ_MAX_LEN_NAME]; // just left it in to make the object not empty
+    //SubGhzNotificationState state_notifications;
+
+    /*SubGhzViewReceiver* subghz_receiver;
+    SubGhzViewTransmitter* subghz_transmitter;
+
+    SubGhzFrequencyAnalyzer* subghz_frequency_analyzer;
+    SubGhzReadRAW* subghz_read_raw;*/
+
+    //SubGhzProtocolFlag filter;
+    //FuriString* error_str;
+    //SubGhzLock lock;
+    //SubGhzThresholdRssi* threshold_rssi;
+    //SubGhzRxKeyState rx_key_state;
+    //SubGhzHistory* history;
+    SubGhzLoadTypeFile load_type_file;
+    //void* rpc_ctx;
+};
+
+//void subghz_set_default_preset(SubGhz* subghz);
+//void subghz_blink_start(SubGhz* subghz);
+//void subghz_blink_stop(SubGhz* subghz);
+
+// Used on Encoded SubGhz
+bool subghz_tx_start(SubGhz* subghz, FlipperFormat* flipper_format);
+//void subghz_dialog_message_show_only_rx(SubGhz* subghz);
+
+bool subghz_key_load(SubGhz* subghz, const char* file_path); //, bool show_dialog);
+bool subghz_load_protocol_from_file(SubGhz* subghz, const char* path);
+//bool subghz_file_available(SubGhz* subghz);
+SubGhzLoadTypeFile subghz_get_load_type_file(SubGhz* subghz);
+
+//void subghz_lock(SubGhz* subghz);
+//void subghz_unlock(SubGhz* subghz);
+//bool subghz_is_locked(SubGhz* subghz);
+
+//void subghz_rx_key_state_set(SubGhz* subghz, SubGhzRxKeyState state);
+//SubGhzRxKeyState subghz_rx_key_state_get(SubGhz* subghz);

+ 630 - 0
cross_remote/helpers/subghz/subghz_txrx.c

@@ -0,0 +1,630 @@
+#include "subghz_txrx_i.h"
+
+#include <lib/subghz/subghz_protocol_registry.h>
+#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+
+#define TAG "SubGhz"
+
+static void subghz_txrx_radio_device_power_on(SubGhzTxRx* instance) {
+    UNUSED(instance);
+    uint8_t attempts = 5;
+    while(--attempts > 0) {
+        if(furi_hal_power_enable_otg()) break;
+    }
+    if(attempts == 0) {
+        if(furi_hal_power_get_usb_voltage() < 4.5f) {
+            FURI_LOG_E(
+                TAG,
+                "Error power otg enable. BQ2589 check otg fault = %d",
+                furi_hal_power_check_otg_fault() ? 1 : 0);
+        }
+    }
+}
+
+static void subghz_txrx_radio_device_power_off(SubGhzTxRx* instance) {
+    UNUSED(instance);
+    if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+}
+
+SubGhzTxRx* subghz_txrx_alloc() {
+    SubGhzTxRx* instance = malloc(sizeof(SubGhzTxRx));
+    instance->setting = subghz_setting_alloc();
+    subghz_setting_load(instance->setting, EXT_PATH("subghz/assets/setting_user"));
+
+    instance->preset = malloc(sizeof(SubGhzRadioPreset));
+    instance->preset->name = furi_string_alloc();
+    subghz_txrx_set_preset(
+        instance, "AM650", subghz_setting_get_default_frequency(instance->setting), NULL, 0);
+
+    instance->txrx_state = SubGhzTxRxStateSleep;
+
+    subghz_txrx_hopper_set_state(instance, SubGhzHopperStateOFF);
+    subghz_txrx_speaker_set_state(instance, SubGhzSpeakerStateDisable);
+
+    instance->worker = subghz_worker_alloc();
+    instance->fff_data = flipper_format_string_alloc();
+
+    instance->environment = subghz_environment_alloc();
+    instance->is_database_loaded =
+        subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_NAME);
+    subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_USER_NAME);
+    subghz_environment_set_came_atomo_rainbow_table_file_name(
+        instance->environment, SUBGHZ_CAME_ATOMO_DIR_NAME);
+    subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
+        instance->environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
+    subghz_environment_set_nice_flor_s_rainbow_table_file_name(
+        instance->environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
+    subghz_environment_set_protocol_registry(
+        instance->environment, (void*)&subghz_protocol_registry);
+    instance->receiver = subghz_receiver_alloc_init(instance->environment);
+
+    subghz_worker_set_overrun_callback(
+        instance->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
+    subghz_worker_set_pair_callback(
+        instance->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
+    subghz_worker_set_context(instance->worker, instance->receiver);
+
+    //set default device External
+    subghz_devices_init();
+    instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
+    instance->radio_device_type =
+        subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101);
+
+    FURI_LOG_D(TAG, "completed TXRX alloc");
+
+    return instance;
+}
+
+void subghz_txrx_free(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    FURI_LOG_D(TAG, "freeing TXRX");
+
+    if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) {
+        subghz_txrx_radio_device_power_off(instance);
+        subghz_devices_end(instance->radio_device);
+    }
+
+    subghz_devices_deinit();
+
+    subghz_worker_free(instance->worker);
+    subghz_receiver_free(instance->receiver);
+    subghz_environment_free(instance->environment);
+    flipper_format_free(instance->fff_data);
+    furi_string_free(instance->preset->name);
+    subghz_setting_free(instance->setting);
+
+    free(instance->preset);
+    free(instance);
+}
+
+/*bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->is_database_loaded;
+}*/
+
+void subghz_txrx_set_preset(
+    SubGhzTxRx* instance,
+    const char* preset_name,
+    uint32_t frequency,
+    uint8_t* preset_data,
+    size_t preset_data_size) {
+    furi_assert(instance);
+    furi_string_set(instance->preset->name, preset_name);
+    SubGhzRadioPreset* preset = instance->preset;
+    preset->frequency = frequency;
+    preset->data = preset_data;
+    preset->data_size = preset_data_size;
+}
+
+const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset) {
+    UNUSED(instance);
+    const char* preset_name = "";
+    if(!strcmp(preset, "FuriHalSubGhzPresetOok270Async")) {
+        preset_name = "AM270";
+    } else if(!strcmp(preset, "FuriHalSubGhzPresetOok650Async")) {
+        preset_name = "AM650";
+    } else if(!strcmp(preset, "FuriHalSubGhzPreset2FSKDev238Async")) {
+        preset_name = "FM238";
+    } else if(!strcmp(preset, "FuriHalSubGhzPreset2FSKDev476Async")) {
+        preset_name = "FM476";
+    } else if(!strcmp(preset, "FuriHalSubGhzPresetCustom")) {
+        preset_name = "CUSTOM";
+    } else {
+        FURI_LOG_E(TAG, "Unknown preset");
+    }
+    return preset_name;
+}
+
+/*SubGhzRadioPreset subghz_txrx_get_preset(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return *instance->preset;
+}*/
+
+/*void subghz_txrx_get_frequency_and_modulation(
+    SubGhzTxRx* instance,
+    FuriString* frequency,
+    FuriString* modulation) {
+    furi_assert(instance);
+    SubGhzRadioPreset* preset = instance->preset;
+    if(frequency != NULL) {
+        furi_string_printf(
+            frequency,
+            "%03ld.%02ld",
+            preset->frequency / 1000000 % 1000,
+            preset->frequency / 10000 % 100);
+    }
+    if(modulation != NULL) {
+        furi_string_printf(modulation, "%.2s", furi_string_get_cstr(preset->name));
+    }
+}*/
+
+static void subghz_txrx_begin(SubGhzTxRx* instance, uint8_t* preset_data) {
+    furi_assert(instance);
+    subghz_devices_reset(instance->radio_device);
+    subghz_devices_idle(instance->radio_device);
+    subghz_devices_load_preset(instance->radio_device, FuriHalSubGhzPresetCustom, preset_data);
+    instance->txrx_state = SubGhzTxRxStateIDLE;
+    FURI_LOG_D(TAG, "completed subghz_txrx_begin");
+}
+
+/*static uint32_t subghz_txrx_rx(SubGhzTxRx* instance, uint32_t frequency) {
+    furi_assert(instance);
+
+    furi_assert(
+        instance->txrx_state != SubGhzTxRxStateRx && instance->txrx_state != SubGhzTxRxStateSleep);
+
+    subghz_devices_idle(instance->radio_device);
+
+    uint32_t value = subghz_devices_set_frequency(instance->radio_device, frequency);
+    subghz_devices_flush_rx(instance->radio_device);
+    subghz_txrx_speaker_on(instance);
+
+    subghz_devices_start_async_rx(
+        instance->radio_device, subghz_worker_rx_callback, instance->worker);
+    subghz_worker_start(instance->worker);
+    instance->txrx_state = SubGhzTxRxStateRx;
+    return value;
+}*/
+
+static void subghz_txrx_idle(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->txrx_state != SubGhzTxRxStateSleep) {
+        subghz_devices_idle(instance->radio_device);
+        subghz_txrx_speaker_off(instance);
+        instance->txrx_state = SubGhzTxRxStateIDLE;
+    }
+    FURI_LOG_D(TAG, "completed subghz_txrx_idle");
+}
+
+/*static void subghz_txrx_rx_end(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    furi_assert(instance->txrx_state == SubGhzTxRxStateRx);
+
+    if(subghz_worker_is_running(instance->worker)) {
+        subghz_worker_stop(instance->worker);
+        subghz_devices_stop_async_rx(instance->radio_device);
+    }
+    subghz_devices_idle(instance->radio_device);
+    subghz_txrx_speaker_off(instance);
+    instance->txrx_state = SubGhzTxRxStateIDLE;
+}*/
+
+/*void subghz_txrx_sleep(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    subghz_devices_sleep(instance->radio_device);
+    instance->txrx_state = SubGhzTxRxStateSleep;
+}*/
+
+static bool subghz_txrx_tx(SubGhzTxRx* instance, uint32_t frequency) {
+    furi_assert(instance);
+    furi_assert(instance->txrx_state != SubGhzTxRxStateSleep);
+    subghz_devices_idle(instance->radio_device);
+    subghz_devices_set_frequency(instance->radio_device, frequency);
+
+    bool ret = subghz_devices_set_tx(instance->radio_device);
+    if(ret) {
+        subghz_txrx_speaker_on(instance);
+        instance->txrx_state = SubGhzTxRxStateTx;
+    }
+
+    FURI_LOG_D(TAG, "completed subghz_txrx_tx");
+    return ret;
+}
+
+SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format) {
+    furi_assert(instance);
+    furi_assert(flipper_format);
+
+    subghz_txrx_stop(instance);
+
+    SubGhzTxRxStartTxState ret = SubGhzTxRxStartTxStateErrorParserOthers;
+    FuriString* temp_str = furi_string_alloc();
+    uint32_t repeat = 200;
+
+    FURI_LOG_D(TAG, "starting loop in subghz_txrx_tx_start");
+    do {
+        FURI_LOG_D(TAG, "looping");
+        if(!flipper_format_rewind(flipper_format)) {
+            FURI_LOG_E(TAG, "Rewind error");
+            break;
+        }
+        if(!flipper_format_read_string(flipper_format, "Protocol", temp_str)) {
+            FURI_LOG_E(TAG, "Missing Protocol");
+            break;
+        }
+        if(!flipper_format_insert_or_update_uint32(flipper_format, "Repeat", &repeat, 1)) {
+            FURI_LOG_E(TAG, "Unable Repeat");
+            break;
+        }
+        //FURI_LOG_D(TAG, "File loaded");
+        ret = SubGhzTxRxStartTxStateOk;
+
+        SubGhzRadioPreset* preset = instance->preset;
+        instance->transmitter =
+            subghz_transmitter_alloc_init(instance->environment, furi_string_get_cstr(temp_str));
+
+        if(instance->transmitter) {
+            FURI_LOG_D(TAG, "transmitter found");
+            if(subghz_transmitter_deserialize(instance->transmitter, flipper_format) ==
+               SubGhzProtocolStatusOk) {
+                //if (false) {
+                FURI_LOG_D(TAG, "deserialization");
+                if(strcmp(furi_string_get_cstr(preset->name), "") != 0) {
+                    FURI_LOG_D(TAG, "got preset name");
+                    subghz_txrx_begin(
+                        instance,
+                        subghz_setting_get_preset_data_by_name(
+                            instance->setting, furi_string_get_cstr(preset->name)));
+                    FURI_LOG_D(TAG, "loaded preset by name");
+                    if(preset->frequency) {
+                        if(!subghz_txrx_tx(instance, preset->frequency)) {
+                            FURI_LOG_E(TAG, "Only Rx");
+                            ret = SubGhzTxRxStartTxStateErrorOnlyRx;
+                        }
+                        FURI_LOG_D(TAG, "got frequency");
+                    } else {
+                        ret = SubGhzTxRxStartTxStateErrorParserOthers;
+                    }
+
+                } else {
+                    FURI_LOG_E(
+                        TAG, "Unknown name preset \" %s \"", furi_string_get_cstr(preset->name));
+                    ret = SubGhzTxRxStartTxStateErrorParserOthers;
+                }
+
+                if(ret == SubGhzTxRxStartTxStateOk) {
+                    //Start TX
+                    FURI_LOG_D(TAG, "starting Async TX");
+                    subghz_devices_start_async_tx(
+                        instance->radio_device, subghz_transmitter_yield, instance->transmitter);
+                }
+            } else {
+                FURI_LOG_D(TAG, "no deserialization");
+                ret = SubGhzTxRxStartTxStateErrorParserOthers;
+            }
+        } else {
+            ret = SubGhzTxRxStartTxStateErrorParserOthers;
+        }
+        if(ret != SubGhzTxRxStartTxStateOk) {
+            FURI_LOG_D(TAG, "state not ok");
+            subghz_transmitter_free(instance->transmitter); // Crashes here
+            if(instance->txrx_state != SubGhzTxRxStateIDLE) {
+                subghz_txrx_idle(instance);
+            }
+        }
+    } while(false);
+    furi_string_free(temp_str);
+    return ret;
+}
+
+/*void subghz_txrx_rx_start(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    subghz_txrx_stop(instance);
+    subghz_txrx_begin(
+        instance,
+        subghz_setting_get_preset_data_by_name(
+            subghz_txrx_get_setting(instance), furi_string_get_cstr(instance->preset->name)));
+    subghz_txrx_rx(instance, instance->preset->frequency);
+}*/
+
+/*void subghz_txrx_set_need_save_callback(
+    SubGhzTxRx* instance,
+    SubGhzTxRxNeedSaveCallback callback,
+    void* context) {
+    furi_assert(instance);
+    instance->need_save_callback = callback;
+    instance->need_save_context = context;
+}*/
+
+static void subghz_txrx_tx_stop(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    furi_assert(instance->txrx_state == SubGhzTxRxStateTx);
+    //Stop TX
+    subghz_devices_stop_async_tx(instance->radio_device);
+    subghz_transmitter_stop(instance->transmitter);
+    subghz_transmitter_free(instance->transmitter);
+
+    //if protocol dynamic then we save the last upload
+    /*if(instance->decoder_result->protocol->type == SubGhzProtocolTypeDynamic) {
+        if(instance->need_save_callback) {
+            instance->need_save_callback(instance->need_save_context);
+        }
+    }*/
+    subghz_txrx_idle(instance);
+    subghz_txrx_speaker_off(instance);
+}
+
+FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->fff_data;
+}
+
+SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->setting;
+}
+
+void subghz_txrx_stop(SubGhzTxRx* instance) {
+    furi_assert(instance);
+
+    switch(instance->txrx_state) {
+    case SubGhzTxRxStateTx:
+        subghz_txrx_tx_stop(instance);
+        subghz_txrx_speaker_unmute(instance);
+        break;
+    case SubGhzTxRxStateRx:
+        //subghz_txrx_rx_end(instance);
+        //subghz_txrx_speaker_mute(instance);
+        break;
+
+    default:
+        break;
+    }
+}
+
+/*void subghz_txrx_hopper_update(SubGhzTxRx* instance) {
+    furi_assert(instance);
+
+    switch(instance->hopper_state) {
+    case SubGhzHopperStateOFF:
+    case SubGhzHopperStatePause:
+        return;
+    case SubGhzHopperStateRSSITimeOut:
+        if(instance->hopper_timeout != 0) {
+            instance->hopper_timeout--;
+            return;
+        }
+        break;
+    default:
+        break;
+    }
+    float rssi = -127.0f;
+    if(instance->hopper_state != SubGhzHopperStateRSSITimeOut) {
+        // See RSSI Calculation timings in CC1101 17.3 RSSI
+        rssi = subghz_devices_get_rssi(instance->radio_device);
+
+        // Stay if RSSI is high enough
+        if(rssi > -90.0f) {
+            instance->hopper_timeout = 10;
+            instance->hopper_state = SubGhzHopperStateRSSITimeOut;
+            return;
+        }
+    } else {
+        instance->hopper_state = SubGhzHopperStateRunnig;
+    }
+    // Select next frequency
+    if(instance->hopper_idx_frequency <
+       subghz_setting_get_hopper_frequency_count(instance->setting) - 1) {
+        instance->hopper_idx_frequency++;
+    } else {
+        instance->hopper_idx_frequency = 0;
+    }
+
+    if(instance->txrx_state == SubGhzTxRxStateRx) {
+        subghz_txrx_rx_end(instance);
+    };
+    if(instance->txrx_state == SubGhzTxRxStateIDLE) {
+        subghz_receiver_reset(instance->receiver);
+        instance->preset->frequency =
+            subghz_setting_get_hopper_frequency(instance->setting, instance->hopper_idx_frequency);
+        subghz_txrx_rx(instance, instance->preset->frequency);
+    }
+}*/
+
+/*SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->hopper_state;
+}*/
+
+void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state) {
+    furi_assert(instance);
+    instance->hopper_state = state;
+}
+
+/*void subghz_txrx_hopper_unpause(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->hopper_state == SubGhzHopperStatePause) {
+        instance->hopper_state = SubGhzHopperStateRunnig;
+    }
+}*/
+
+/*void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->hopper_state == SubGhzHopperStateRunnig) {
+        instance->hopper_state = SubGhzHopperStatePause;
+    }
+}*/
+
+void subghz_txrx_speaker_on(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_acquire(30)) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker);
+        } else {
+            instance->speaker_state = SubGhzSpeakerStateDisable;
+        }
+    }
+}
+
+void subghz_txrx_speaker_off(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state != SubGhzSpeakerStateDisable) {
+        if(furi_hal_speaker_is_mine()) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+            furi_hal_speaker_release();
+            if(instance->speaker_state == SubGhzSpeakerStateShutdown)
+                instance->speaker_state = SubGhzSpeakerStateDisable;
+        }
+    }
+}
+
+/*void subghz_txrx_speaker_mute(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+        }
+    }
+}*/
+
+void subghz_txrx_speaker_unmute(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker);
+        }
+    }
+}
+
+void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state) {
+    furi_assert(instance);
+    instance->speaker_state = state;
+}
+
+/*SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->speaker_state;
+}*/
+
+bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol) {
+    furi_assert(instance);
+    furi_assert(name_protocol);
+    bool res = false;
+    instance->decoder_result =
+        subghz_receiver_search_decoder_base_by_name(instance->receiver, name_protocol);
+    if(instance->decoder_result) {
+        res = true;
+    }
+    return res;
+}
+
+SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->decoder_result;
+}
+
+/*bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return (
+        (instance->decoder_result->protocol->flag & SubGhzProtocolFlag_Save) ==
+        SubGhzProtocolFlag_Save);
+}*/
+
+/*bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type) {
+    furi_assert(instance);
+    const SubGhzProtocol* protocol = instance->decoder_result->protocol;
+    if(check_type) {
+        return (
+            ((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) &&
+            protocol->encoder->deserialize && protocol->type == SubGhzProtocolTypeStatic);
+    }
+    return (
+        ((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) &&
+        protocol->encoder->deserialize);
+}*/
+
+/*void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter) {
+    furi_assert(instance);
+    subghz_receiver_set_filter(instance->receiver, filter);
+}*/
+
+/*void subghz_txrx_set_rx_calback(
+    SubGhzTxRx* instance,
+    SubGhzReceiverCallback callback,
+    void* context) {
+    subghz_receiver_set_rx_callback(instance->receiver, callback, context);
+}*/
+
+void subghz_txrx_set_raw_file_encoder_worker_callback_end(
+    SubGhzTxRx* instance,
+    SubGhzProtocolEncoderRAWCallbackEnd callback,
+    void* context) {
+    subghz_protocol_raw_file_encoder_worker_set_callback_end(
+        (SubGhzProtocolEncoderRAW*)subghz_transmitter_get_protocol_instance(instance->transmitter),
+        callback,
+        context);
+}
+
+bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name) {
+    furi_assert(instance);
+
+    bool is_connect = false;
+    bool is_otg_enabled = furi_hal_power_is_otg_enabled();
+
+    if(!is_otg_enabled) {
+        subghz_txrx_radio_device_power_on(instance);
+    }
+
+    const SubGhzDevice* device = subghz_devices_get_by_name(name);
+    if(device) {
+        is_connect = subghz_devices_is_connect(device);
+    }
+
+    if(!is_otg_enabled) {
+        subghz_txrx_radio_device_power_off(instance);
+    }
+    return is_connect;
+}
+
+SubGhzRadioDeviceType
+    subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type) {
+    furi_assert(instance);
+
+    if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
+       subghz_txrx_radio_device_is_external_connected(instance, SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
+        subghz_txrx_radio_device_power_on(instance);
+        instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
+        subghz_devices_begin(instance->radio_device);
+        instance->radio_device_type = SubGhzRadioDeviceTypeExternalCC1101;
+    } else {
+        subghz_txrx_radio_device_power_off(instance);
+        if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) {
+            subghz_devices_end(instance->radio_device);
+        }
+        instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+        instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
+    }
+
+    return instance->radio_device_type;
+}
+
+/*SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->radio_device_type;
+}*/
+
+/*float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return subghz_devices_get_rssi(instance->radio_device);
+}*/
+
+const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return subghz_devices_get_name(instance->radio_device);
+}
+
+bool subghz_txrx_radio_device_is_frequecy_valid(SubGhzTxRx* instance, uint32_t frequency) {
+    furi_assert(instance);
+    return subghz_devices_is_frequency_valid(instance->radio_device, frequency);
+}

+ 336 - 0
cross_remote/helpers/subghz/subghz_txrx.h

@@ -0,0 +1,336 @@
+#pragma once
+
+#include "subghz_types.h"
+
+#include <lib/subghz/subghz_worker.h>
+#include <lib/subghz/subghz_setting.h>
+#include <lib/subghz/receiver.h>
+#include <lib/subghz/transmitter.h>
+#include <lib/subghz/protocols/raw.h>
+#include <lib/subghz/devices/devices.h>
+
+typedef struct SubGhzTxRx SubGhzTxRx;
+
+typedef void (*SubGhzTxRxNeedSaveCallback)(void* context);
+
+typedef enum {
+    SubGhzTxRxStartTxStateOk,
+    SubGhzTxRxStartTxStateErrorOnlyRx,
+    SubGhzTxRxStartTxStateErrorParserOthers,
+} SubGhzTxRxStartTxState;
+
+/**
+ * Allocate SubGhzTxRx
+ * 
+ * @return SubGhzTxRx* pointer to SubGhzTxRx
+ */
+SubGhzTxRx* subghz_txrx_alloc();
+
+/**
+ * Free SubGhzTxRx
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+void subghz_txrx_free(SubGhzTxRx* instance);
+
+/**
+ * Check if the database is loaded
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @return bool True if the database is loaded
+ */
+//bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance);
+
+/**
+ * Set preset 
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param preset_name Name of preset
+ * @param frequency Frequency in Hz
+ * @param preset_data Data of preset
+ * @param preset_data_size Size of preset data
+ */
+void subghz_txrx_set_preset(
+    SubGhzTxRx* instance,
+    const char* preset_name,
+    uint32_t frequency,
+    uint8_t* preset_data,
+    size_t preset_data_size);
+
+/**
+ * Get name of preset
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param preset String of preset 
+ * @return const char*  Name of preset
+ */
+const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset);
+
+/**
+ * Get of preset
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @return SubGhzRadioPreset Preset
+ */
+//SubGhzRadioPreset subghz_txrx_get_preset(SubGhzTxRx* instance);
+
+/**
+ * Get string frequency and modulation
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param frequency Pointer to a string frequency
+ * @param modulation Pointer to a string modulation
+ */
+/*void subghz_txrx_get_frequency_and_modulation(
+    SubGhzTxRx* instance,
+    FuriString* frequency,
+    FuriString* modulation);*/
+
+/**
+ * Start TX CC1101
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param flipper_format Pointer to a FlipperFormat
+ * @return SubGhzTxRxStartTxState 
+ */
+SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format);
+
+/**
+ * Start RX CC1101
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+//void subghz_txrx_rx_start(SubGhzTxRx* instance);
+
+/**
+ * Stop TX/RX CC1101
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+void subghz_txrx_stop(SubGhzTxRx* instance);
+
+/**
+ * Set sleep mode CC1101
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+//void subghz_txrx_sleep(SubGhzTxRx* instance);
+
+/**
+ * Update frequency CC1101 in automatic mode (hopper)
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+//void subghz_txrx_hopper_update(SubGhzTxRx* instance);
+
+/**
+ * Get state hopper
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @return SubGhzHopperState 
+ */
+//SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance);
+
+/**
+ * Set state hopper
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param state State hopper
+ */
+void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state);
+
+/**
+ * Unpause hopper
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+//void subghz_txrx_hopper_unpause(SubGhzTxRx* instance);
+
+/**
+ * Set pause hopper
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ */
+//void subghz_txrx_hopper_pause(SubGhzTxRx* instance);
+
+/**
+ * Speaker on
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ */
+void subghz_txrx_speaker_on(SubGhzTxRx* instance);
+
+/**
+ * Speaker off
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ */
+void subghz_txrx_speaker_off(SubGhzTxRx* instance);
+
+/**
+ * Speaker mute
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ */
+//void subghz_txrx_speaker_mute(SubGhzTxRx* instance);
+
+/**
+ * Speaker unmute
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ */
+void subghz_txrx_speaker_unmute(SubGhzTxRx* instance);
+
+/**
+ * Set state speaker
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ * @param state State speaker
+ */
+void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state);
+
+/**
+ * Get state speaker
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ * @return SubGhzSpeakerState 
+ */
+//SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance);
+
+/**
+ * load decoder by name protocol
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param name_protocol Name protocol
+ * @return bool True if the decoder is loaded 
+ */
+bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol);
+
+/**
+ * Get decoder
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @return SubGhzProtocolDecoderBase* Pointer to a SubGhzProtocolDecoderBase
+ */
+SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance);
+
+/**
+ * Set callback for save data
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param callback Callback for save data
+ * @param context Context for callback
+ */
+/*void subghz_txrx_set_need_save_callback(
+    SubGhzTxRx* instance,
+    SubGhzTxRxNeedSaveCallback callback,
+    void* context);*/
+
+/**
+ * Get pointer to a load data key
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @return FlipperFormat* 
+ */
+FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance);
+
+/**
+ * Get pointer to a SugGhzSetting
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @return SubGhzSetting* 
+ */
+SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance);
+
+/**
+ * Is it possible to save this protocol
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ * @return bool True if it is possible to save this protocol
+ */
+//bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance);
+
+/**
+ * Is it possible to send this protocol
+ * 
+ * @param instance Pointer to a SubGhzTxRx 
+ * @return bool True if it is possible to send this protocol
+ */
+//bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type);
+
+/**
+ * Set filter, what types of decoder to use 
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param filter Filter
+ */
+//void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter);
+
+/**
+ * Set callback for receive data
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param callback Callback for receive data
+ * @param context Context for callback
+ */
+/*void subghz_txrx_set_rx_calback(
+    SubGhzTxRx* instance,
+    SubGhzReceiverCallback callback,
+    void* context);*/
+
+/**
+ * Set callback for Raw decoder, end of data transfer  
+ * 
+ * @param instance Pointer to a SubGhzTxRx
+ * @param callback Callback for Raw decoder, end of data transfer 
+ * @param context Context for callback
+ */
+void subghz_txrx_set_raw_file_encoder_worker_callback_end(
+    SubGhzTxRx* instance,
+    SubGhzProtocolEncoderRAWCallbackEnd callback,
+    void* context);
+
+/* Checking if an external radio device is connected
+* 
+* @param instance Pointer to a SubGhzTxRx
+* @param name Name of external radio device
+* @return bool True if is connected to the external radio device
+*/
+bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name);
+
+/* Set the selected radio device to use
+*
+* @param instance Pointer to a SubGhzTxRx
+* @param radio_device_type Radio device type
+* @return SubGhzRadioDeviceType Type of installed radio device
+*/
+SubGhzRadioDeviceType
+    subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type);
+
+/* Get the selected radio device to use
+*
+* @param instance Pointer to a SubGhzTxRx
+* @return SubGhzRadioDeviceType Type of installed radio device
+*/
+//SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance);
+
+/* Get RSSI the selected radio device to use
+*
+* @param instance Pointer to a SubGhzTxRx
+* @return float RSSI
+*/
+//float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance);
+
+/* Get name the selected radio device to use
+*
+* @param instance Pointer to a SubGhzTxRx
+* @return const char* Name of installed radio device
+*/
+const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance);
+
+/* Get get intelligence whether frequency the selected radio device to use
+*
+* @param instance Pointer to a SubGhzTxRx
+* @return bool True if the frequency is valid
+*/
+bool subghz_txrx_radio_device_is_frequecy_valid(SubGhzTxRx* instance, uint32_t frequency);

+ 29 - 0
cross_remote/helpers/subghz/subghz_txrx_i.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include "subghz_txrx.h"
+
+struct SubGhzTxRx {
+    SubGhzWorker* worker;
+
+    SubGhzEnvironment* environment;
+    SubGhzReceiver* receiver;
+    SubGhzTransmitter* transmitter;
+    SubGhzProtocolDecoderBase* decoder_result;
+    FlipperFormat* fff_data;
+
+    SubGhzRadioPreset* preset;
+    SubGhzSetting* setting;
+
+    uint8_t hopper_timeout;
+    uint8_t hopper_idx_frequency;
+    bool is_database_loaded;
+    SubGhzHopperState hopper_state;
+
+    SubGhzTxRxState txrx_state;
+    SubGhzSpeakerState speaker_state;
+    const SubGhzDevice* radio_device;
+    SubGhzRadioDeviceType radio_device_type;
+
+    SubGhzTxRxNeedSaveCallback need_save_callback;
+    void* need_save_context;
+};

+ 77 - 0
cross_remote/helpers/subghz/subghz_types.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+/** SubGhzNotification state */
+typedef enum {
+    SubGhzNotificationStateStarting,
+    SubGhzNotificationStateIDLE,
+    SubGhzNotificationStateTx,
+    SubGhzNotificationStateRx,
+    SubGhzNotificationStateRxDone,
+} SubGhzNotificationState;
+
+/** SubGhzTxRx state */
+typedef enum {
+    SubGhzTxRxStateIDLE,
+    SubGhzTxRxStateRx,
+    SubGhzTxRxStateTx,
+    SubGhzTxRxStateSleep,
+} SubGhzTxRxState;
+
+/** SubGhzHopperState state */
+typedef enum {
+    SubGhzHopperStateOFF,
+    SubGhzHopperStateRunnig,
+    SubGhzHopperStatePause,
+    SubGhzHopperStateRSSITimeOut,
+} SubGhzHopperState;
+
+/** SubGhzSpeakerState state */
+typedef enum {
+    SubGhzSpeakerStateDisable,
+    SubGhzSpeakerStateShutdown,
+    SubGhzSpeakerStateEnable,
+} SubGhzSpeakerState;
+
+/** SubGhzRadioDeviceType */
+typedef enum {
+    SubGhzRadioDeviceTypeAuto,
+    SubGhzRadioDeviceTypeInternal,
+    SubGhzRadioDeviceTypeExternalCC1101,
+} SubGhzRadioDeviceType;
+
+/** SubGhzRxKeyState state */
+typedef enum {
+    SubGhzRxKeyStateIDLE,
+    SubGhzRxKeyStateNoSave,
+    SubGhzRxKeyStateNeedSave,
+    SubGhzRxKeyStateBack,
+    SubGhzRxKeyStateStart,
+    SubGhzRxKeyStateAddKey,
+    SubGhzRxKeyStateExit,
+    SubGhzRxKeyStateRAWLoad,
+    SubGhzRxKeyStateRAWSave,
+} SubGhzRxKeyState;
+
+/** SubGhzLoadKeyState state */
+typedef enum {
+    SubGhzLoadKeyStateUnknown,
+    SubGhzLoadKeyStateOK,
+    SubGhzLoadKeyStateParseErr,
+    SubGhzLoadKeyStateProtocolDescriptionErr,
+} SubGhzLoadKeyState;
+
+/** SubGhzLock */
+typedef enum {
+    SubGhzLockOff,
+    SubGhzLockOn,
+} SubGhzLock;
+
+/** SubGhz load type file */
+typedef enum {
+    SubGhzLoadTypeFileNoLoad,
+    SubGhzLoadTypeFileKey,
+    SubGhzLoadTypeFileRaw,
+} SubGhzLoadTypeFile;

+ 79 - 0
cross_remote/helpers/xremote_custom_event.h

@@ -0,0 +1,79 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+enum XRemoteCustomEventType {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    XRemoteCustomEventMenuVoid,
+    XRemoteCustomEventMenuSelected,
+    XRemoteCustomEventMenuAddSelected,
+    XRemoteCustomEventMenuAddIrSelected,
+};
+
+#pragma pack(push, 1)
+typedef union {
+    uint32_t packed_value;
+    struct {
+        uint16_t type;
+        int16_t value;
+    } content;
+} XRemoteCustomEventMenu;
+#pragma pack(pop)
+
+typedef enum {
+    XRemoteCustomEventInfoscreenUp,
+    XRemoteCustomEventInfoscreenDown,
+    XRemoteCustomEventInfoscreenLeft,
+    XRemoteCustomEventInfoscreenRight,
+    XRemoteCustomEventInfoscreenOk,
+    XRemoteCustomEventInfoscreenBack,
+
+    XRemoteCustomEventCreateUp,
+    XRemoteCustomEventCreateDown,
+    XRemoteCustomEventCreateLeft,
+    XRemoteCustomEventCreateRight,
+    XRemoteCustomEventCreateOk,
+    XRemoteCustomEventCreateBack,
+
+    XRemoteCustomEventScene2Up,
+    XRemoteCustomEventScene2Down,
+    XRemoteCustomEventScene2Left,
+    XRemoteCustomEventScene2Right,
+    XRemoteCustomEventScene2Ok,
+    XRemoteCustomEventScene2Back,
+
+    XRemoteCustomEventTypePopupClosed,
+    XRemoteCustomEventTextInput,
+
+    XRemoteCustomEventPauseSetBack,
+    XRemoteCustomEventPauseSetUp,
+    XRemoteCustomEventPauseSetDown,
+    XRemoteCustomEventPauseSetOk,
+
+    XRemoteCustomEventViewTransmitterSendStop,
+} XRemoteCustomEvent;
+
+static inline uint32_t xremote_custom_menu_event_pack(uint16_t type, int16_t value) {
+    XRemoteCustomEventMenu event = {.content = {.type = type, .value = value}};
+    return event.packed_value;
+}
+
+static inline void
+    xremote_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) {
+    XRemoteCustomEventMenu event = {.packed_value = packed_value};
+    if(type) *type = event.content.type;
+    if(value) *value = event.content.value;
+}
+
+static inline uint16_t xremote_custom_menu_event_get_type(uint32_t packed_value) {
+    uint16_t type;
+    xremote_custom_menu_event_unpack(packed_value, &type, NULL);
+    return type;
+}
+
+static inline int16_t xremote_custom_menu_event_get_value(uint32_t packed_value) {
+    int16_t value;
+    xremote_custom_menu_event_unpack(packed_value, NULL, &value);
+    return value;
+}

+ 35 - 0
cross_remote/helpers/xremote_haptic.c

@@ -0,0 +1,35 @@
+#include "xremote_haptic.h"
+#include "../xremote.h"
+
+void xremote_play_happy_bump(void* context) {
+    XRemote* app = context;
+    if(app->haptic != 1) {
+        return;
+    }
+    notification_message(app->notification, &sequence_set_vibro_on);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 20);
+    notification_message(app->notification, &sequence_reset_vibro);
+}
+
+void xremote_play_bad_bump(void* context) {
+    XRemote* app = context;
+    if(app->haptic != 1) {
+        return;
+    }
+    notification_message(app->notification, &sequence_set_vibro_on);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
+    notification_message(app->notification, &sequence_reset_vibro);
+}
+
+void xremote_play_long_bump(void* context) {
+    XRemote* app = context;
+    if(app->haptic != 1) {
+        return;
+    }
+    for(int i = 0; i < 4; i++) {
+        notification_message(app->notification, &sequence_set_vibro_on);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 50);
+        notification_message(app->notification, &sequence_reset_vibro);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
+    }
+}

+ 9 - 0
cross_remote/helpers/xremote_haptic.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include <notification/notification_messages.h>
+
+void xremote_play_happy_bump(void* context);
+
+void xremote_play_bad_bump(void* context);
+
+void xremote_play_long_bump(void* context);

+ 38 - 0
cross_remote/helpers/xremote_led.c

@@ -0,0 +1,38 @@
+#include "xremote_led.h"
+
+void xremote_led_set_rgb(void* context, int red, int green, int blue) {
+    XRemote* app = context;
+    if(app->led != 1) {
+        return;
+    }
+    NotificationMessage notification_led_message_1;
+    notification_led_message_1.type = NotificationMessageTypeLedRed;
+    NotificationMessage notification_led_message_2;
+    notification_led_message_2.type = NotificationMessageTypeLedGreen;
+    NotificationMessage notification_led_message_3;
+    notification_led_message_3.type = NotificationMessageTypeLedBlue;
+
+    notification_led_message_1.data.led.value = red;
+    notification_led_message_2.data.led.value = green;
+    notification_led_message_3.data.led.value = blue;
+    const NotificationSequence notification_sequence = {
+        &notification_led_message_1,
+        &notification_led_message_2,
+        &notification_led_message_3,
+        &message_do_not_reset,
+        NULL,
+    };
+    notification_message(app->notification, &notification_sequence);
+    furi_thread_flags_wait(
+        0, FuriFlagWaitAny, 10); //Delay, prevent removal from RAM before LED value set
+}
+
+void xremote_led_reset(void* context) {
+    XRemote* app = context;
+    notification_message(app->notification, &sequence_reset_red);
+    notification_message(app->notification, &sequence_reset_green);
+    notification_message(app->notification, &sequence_reset_blue);
+
+    furi_thread_flags_wait(
+        0, FuriFlagWaitAny, 300); //Delay, prevent removal from RAM before LED value set
+}

+ 7 - 0
cross_remote/helpers/xremote_led.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include "../xremote.h"
+
+void xremote_led_set_rgb(void* context, int red, int green, int blue);
+
+void xremote_led_reset(void* context);

+ 25 - 0
cross_remote/helpers/xremote_speaker.c

@@ -0,0 +1,25 @@
+#include "xremote_speaker.h"
+
+#define NOTE_INPUT 587.33f
+
+void xremote_play_input_sound(void* context) {
+    XRemote* app = context;
+    if(app->speaker != 1) {
+        return;
+    }
+    float volume = 1.0f;
+    if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
+        furi_hal_speaker_start(NOTE_INPUT, volume);
+    }
+}
+
+void xremote_stop_all_sound(void* context) {
+    XRemote* app = context;
+    if(app->speaker != 1) {
+        return;
+    }
+    if(furi_hal_speaker_is_mine()) {
+        furi_hal_speaker_stop();
+        furi_hal_speaker_release();
+    }
+}

+ 8 - 0
cross_remote/helpers/xremote_speaker.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include "../xremote.h"
+
+#define NOTE_INPUT 587.33f
+
+void xremote_play_input_sound(void* context);
+void xremote_stop_all_sound(void* context);

+ 122 - 0
cross_remote/helpers/xremote_storage.c

@@ -0,0 +1,122 @@
+#include "xremote_storage.h"
+
+static Storage* xremote_open_storage() {
+    return furi_record_open(RECORD_STORAGE);
+}
+
+static void xremote_close_storage() {
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void xremote_close_config_file(FlipperFormat* file) {
+    if(file == NULL) return;
+    flipper_format_file_close(file);
+    flipper_format_free(file);
+}
+
+void xremote_save_settings(void* context) {
+    XRemote* app = context;
+    if(app->save_settings == 0) {
+        return;
+    }
+
+    FURI_LOG_D(TAG, "Saving Settings");
+    Storage* storage = xremote_open_storage();
+    FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+    // Overwrite wont work, so delete first
+    if(storage_file_exists(storage, XREMOTE_SETTINGS_SAVE_PATH)) {
+        storage_simply_remove(storage, XREMOTE_SETTINGS_SAVE_PATH);
+    }
+
+    // Open File, create if not exists
+    if(!storage_common_stat(storage, XREMOTE_SETTINGS_SAVE_PATH, NULL) == FSE_OK) {
+        FURI_LOG_D(
+            TAG, "Config file %s is not found. Will create new.", XREMOTE_SETTINGS_SAVE_PATH);
+        if(storage_common_stat(storage, CONFIG_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) {
+            FURI_LOG_D(
+                TAG, "Directory %s doesn't exist. Will create new.", CONFIG_FILE_DIRECTORY_PATH);
+            if(!storage_simply_mkdir(storage, CONFIG_FILE_DIRECTORY_PATH)) {
+                FURI_LOG_E(TAG, "Error creating directory %s", CONFIG_FILE_DIRECTORY_PATH);
+            }
+        }
+    }
+
+    if(!flipper_format_file_open_new(fff_file, XREMOTE_SETTINGS_SAVE_PATH)) {
+        //totp_close_config_file(fff_file);
+        FURI_LOG_E(TAG, "Error creating new file %s", XREMOTE_SETTINGS_SAVE_PATH);
+        xremote_close_storage();
+        return;
+    }
+
+    // Store Settings
+    flipper_format_write_header_cstr(
+        fff_file, XREMOTE_SETTINGS_HEADER, XREMOTE_SETTINGS_FILE_VERSION);
+    flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
+    flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
+    flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_LED, &app->led, 1);
+    flipper_format_write_uint32(
+        fff_file, XREMOTE_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1);
+    flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_IR_TIMING, &app->ir_timing, 1);
+    flipper_format_write_uint32(fff_file, XREMOTE_SETTINGS_KEY_SG_TIMING, &app->sg_timing, 1);
+
+    if(!flipper_format_rewind(fff_file)) {
+        xremote_close_config_file(fff_file);
+        FURI_LOG_E(TAG, "Rewind error");
+        xremote_close_storage();
+        return;
+    }
+
+    xremote_close_config_file(fff_file);
+    xremote_close_storage();
+}
+
+void xremote_read_settings(void* context) {
+    XRemote* app = context;
+    Storage* storage = xremote_open_storage();
+    FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+    if(storage_common_stat(storage, XREMOTE_SETTINGS_SAVE_PATH, NULL) != FSE_OK) {
+        xremote_close_config_file(fff_file);
+        xremote_close_storage();
+        return;
+    }
+    uint32_t file_version;
+    FuriString* temp_str = furi_string_alloc();
+
+    if(!flipper_format_file_open_existing(fff_file, XREMOTE_SETTINGS_SAVE_PATH)) {
+        FURI_LOG_E(TAG, "Cannot open file %s", XREMOTE_SETTINGS_SAVE_PATH);
+        xremote_close_config_file(fff_file);
+        xremote_close_storage();
+        return;
+    }
+
+    if(!flipper_format_read_header(fff_file, temp_str, &file_version)) {
+        FURI_LOG_E(TAG, "Missing Header Data");
+        xremote_close_config_file(fff_file);
+        xremote_close_storage();
+        return;
+    }
+
+    if(file_version < XREMOTE_SETTINGS_FILE_VERSION) {
+        FURI_LOG_I(TAG, "old config version, will be removed.");
+        xremote_close_config_file(fff_file);
+        xremote_close_storage();
+        return;
+    }
+
+    flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
+    flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
+    flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_LED, &app->led, 1);
+    flipper_format_read_uint32(
+        fff_file, XREMOTE_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1);
+    flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_IR_TIMING, &app->ir_timing, 1);
+    flipper_format_read_uint32(fff_file, XREMOTE_SETTINGS_KEY_SG_TIMING, &app->sg_timing, 1);
+
+    flipper_format_rewind(fff_file);
+
+    furi_string_free(temp_str);
+
+    xremote_close_config_file(fff_file);
+    xremote_close_storage();
+}

+ 18 - 0
cross_remote/helpers/xremote_storage.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include "../xremote.h"
+
+#define XREMOTE_SETTINGS_FILE_VERSION 2
+#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/xremote")
+#define XREMOTE_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/xremote.conf"
+#define XREMOTE_SETTINGS_SAVE_PATH_TMP XREMOTE_SETTINGS_SAVE_PATH ".tmp"
+#define XREMOTE_SETTINGS_HEADER "Xremote Config File"
+#define XREMOTE_SETTINGS_KEY_HAPTIC "Haptic"
+#define XREMOTE_SETTINGS_KEY_LED "Led"
+#define XREMOTE_SETTINGS_KEY_SPEAKER "Speaker"
+#define XREMOTE_SETTINGS_KEY_SAVE_SETTINGS "SaveSettings"
+#define XREMOTE_SETTINGS_KEY_IR_TIMING "IRTiming"
+#define XREMOTE_SETTINGS_KEY_SG_TIMING "SGTiming"
+
+void xremote_save_settings(void* context);
+void xremote_read_settings(void* context);

BIN
cross_remote/icons/ButtonDown_10x5.png


BIN
cross_remote/icons/ButtonUp_10x5.png


BIN
cross_remote/icons/KeyBackspaceSelected_16x9.png


BIN
cross_remote/icons/KeyBackspace_16x9.png


BIN
cross_remote/icons/KeySaveSelected_24x11.png


BIN
cross_remote/icons/KeySave_24x11.png


BIN
cross_remote/icons/ir_10px.png


BIN
cross_remote/icons/ir_transmit_128x64.png


BIN
cross_remote/icons/pause_128x64.png


BIN
cross_remote/icons/sg_10px.png


BIN
cross_remote/icons/sg_transmit_128x64.png


BIN
cross_remote/icons/xr_10px.png


BIN
cross_remote/icons/xremote_10px.png


+ 276 - 0
cross_remote/models/cross/xremote_cross_remote.c

@@ -0,0 +1,276 @@
+#include "xremote_cross_remote.h"
+
+ARRAY_DEF(CrossRemoteItemArray, CrossRemoteItem*, M_PTR_OPLIST);
+
+struct CrossRemote {
+    FuriString* name;
+    FuriString* path;
+    CrossRemoteItemArray_t items;
+    int transmitting;
+};
+
+static void xremote_cross_remote_clear_items(CrossRemote* remote) {
+    CrossRemoteItemArray_it_t it;
+    for(CrossRemoteItemArray_it(it, remote->items); !CrossRemoteItemArray_end_p(it);
+        CrossRemoteItemArray_next(it)) {
+        xremote_cross_remote_item_free(*CrossRemoteItemArray_cref(it));
+    }
+    CrossRemoteItemArray_reset(remote->items);
+}
+
+static void xremote_cross_remote_find_vacant_remote_name(FuriString* name, const char* path) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    FuriString* base_path;
+    base_path = furi_string_alloc_set(path);
+
+    if(furi_string_end_with(base_path, XREMOTE_APP_EXTENSION)) {
+        size_t filename_start = furi_string_search_rchar(base_path, '/');
+        furi_string_left(base_path, filename_start);
+    }
+
+    furi_string_printf(
+        base_path, "%s/%s%s", path, furi_string_get_cstr(name), XREMOTE_APP_EXTENSION);
+
+    FS_Error status = storage_common_stat(storage, furi_string_get_cstr(base_path), NULL);
+
+    if(status == FSE_OK) {
+        // If name is taken, try another name2, name3 etc
+        size_t dot = furi_string_search_rchar(base_path, '.');
+        furi_string_left(base_path, dot);
+
+        FuriString* path_temp;
+        path_temp = furi_string_alloc();
+
+        uint32_t i = 1;
+        do {
+            furi_string_printf(
+                path_temp, "%s%lu%s", furi_string_get_cstr(base_path), ++i, XREMOTE_APP_EXTENSION);
+            status = storage_common_stat(storage, furi_string_get_cstr(path_temp), NULL);
+        } while(status == FSE_OK);
+
+        furi_string_free(path_temp);
+
+        if(status == FSE_NOT_EXIST) {
+            furi_string_cat_printf(name, "%lu", i);
+        }
+    }
+
+    furi_string_free(base_path);
+    furi_record_close(RECORD_STORAGE);
+}
+
+CrossRemote* xremote_cross_remote_alloc() {
+    CrossRemote* remote = malloc(sizeof(CrossRemote));
+    CrossRemoteItemArray_init(remote->items);
+    remote->name = furi_string_alloc();
+    remote->path = furi_string_alloc();
+    remote->transmitting = 0;
+    return remote;
+}
+
+void xremote_cross_remote_set_transmitting(CrossRemote* remote, int status) {
+    remote->transmitting = status;
+}
+
+int xremote_cross_remote_get_transmitting(CrossRemote* remote) {
+    return remote->transmitting;
+}
+
+void xremote_cross_remote_free(CrossRemote* remote) {
+    furi_string_free(remote->name);
+    furi_string_free(remote->path);
+    xremote_cross_remote_clear_items(remote);
+    CrossRemoteItemArray_clear(remote->items);
+    free(remote);
+}
+
+const char* xremote_cross_remote_get_name(CrossRemote* remote) {
+    return furi_string_get_cstr(remote->name);
+}
+
+bool xremote_cross_remote_add_ir_item(
+    CrossRemote* remote,
+    const char* name,
+    InfraredSignal* signal,
+    uint32_t timing) {
+    CrossRemoteItem* item = xremote_cross_remote_item_alloc();
+    xremote_cross_remote_item_set_type(item, XRemoteRemoteItemTypeInfrared);
+    xremote_cross_remote_item_set_name(item, name);
+    xremote_cross_remote_item_set_time(item, timing);
+    xremote_cross_remote_item_set_ir_signal(item, signal);
+    CrossRemoteItemArray_push_back(remote->items, item);
+    return true;
+}
+
+bool xremote_cross_remote_add_pause(CrossRemote* remote, int time) {
+    CrossRemoteItem* item = xremote_cross_remote_item_alloc();
+    xremote_cross_remote_item_set_type(item, XRemoteRemoteItemTypePause);
+    char name[9];
+    snprintf(name, 9, CROSS_REMOTE_PAUSE_NAME, time);
+    xremote_cross_remote_item_set_name(item, name);
+    xremote_cross_remote_item_set_time(item, time);
+    CrossRemoteItemArray_push_back(remote->items, item);
+    return true;
+}
+
+bool xremote_cross_remote_add_subghz(CrossRemote* remote, SubGhzRemote* subghz) {
+    CrossRemoteItem* item = xremote_cross_remote_item_alloc();
+    xremote_cross_remote_item_set_type(item, XRemoteRemoteItemTypeSubGhz);
+    xremote_cross_remote_item_set_name(item, xremote_sg_remote_get_name(subghz));
+    xremote_cross_remote_item_set_filename(item, xremote_sg_remote_get_filename(subghz));
+    FURI_LOG_D(TAG, "add subghz: %s", xremote_sg_remote_get_filename(subghz));
+    xremote_cross_remote_item_set_sg_signal(item, subghz);
+
+    CrossRemoteItemArray_push_back(remote->items, item);
+    return true;
+}
+
+size_t xremote_cross_remote_get_item_count(CrossRemote* remote) {
+    return CrossRemoteItemArray_size(remote->items);
+}
+
+CrossRemoteItem* xremote_cross_remote_get_item(CrossRemote* remote, size_t index) {
+    furi_assert(index < CrossRemoteItemArray_size(remote->items));
+    return *CrossRemoteItemArray_get(remote->items, index);
+}
+
+void xremote_cross_remote_remove_item(CrossRemote* remote, size_t index) {
+    CrossRemoteItemArray_erase(remote->items, index);
+}
+
+void xremote_cross_remote_rename_item(CrossRemote* remote, size_t index, const char* name) {
+    CrossRemoteItem* item = xremote_cross_remote_get_item(remote, index);
+    xremote_cross_remote_item_set_name(item, name);
+}
+
+int16_t xremote_cross_remote_get_item_type(CrossRemote* remote, size_t index) {
+    CrossRemoteItem* item = xremote_cross_remote_get_item(remote, index);
+    return xremote_cross_remote_item_get_type(item);
+}
+
+static void xremote_cross_remote_set_name(CrossRemote* remote, const char* name) {
+    furi_string_set(remote->name, name);
+}
+
+static void xremote_cross_remote_set_path(CrossRemote* remote, const char* path) {
+    furi_string_set(remote->path, path);
+}
+
+bool xremote_cross_remote_load(CrossRemote* remote, FuriString* path) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+    FuriString* buf;
+    buf = furi_string_alloc();
+
+    FURI_LOG_I(TAG, "loading file: \'%s\'", furi_string_get_cstr(path));
+    bool success = false;
+    do {
+        // File not found
+        if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path))) break;
+        uint32_t version;
+        // Read Version & Type
+        if(!flipper_format_read_header(ff, buf, &version)) break;
+        if(!furi_string_equal(buf, XREMOTE_FILE_TYPE) || (version != XREMOTE_FILE_VERSION)) break;
+
+        // Init Remote
+        path_extract_filename(path, buf, true);
+        xremote_cross_remote_clear_items(remote);
+        xremote_cross_remote_set_name(remote, furi_string_get_cstr(buf));
+        xremote_cross_remote_set_path(remote, furi_string_get_cstr(path));
+        // Load Items
+        for(bool can_read = true; can_read;) {
+            CrossRemoteItem* item = xremote_cross_remote_item_alloc();
+            can_read = xremote_cross_remote_item_read(item, ff);
+            if(can_read) {
+                CrossRemoteItemArray_push_back(remote->items, item);
+            } else {
+                xremote_cross_remote_item_free(item);
+            }
+        }
+        success = true;
+    } while(false);
+
+    furi_string_free(buf);
+    flipper_format_buffered_file_close(ff);
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}
+
+static bool xremote_cross_remote_store(CrossRemote* remote) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_file_alloc(storage);
+    const char* path = furi_string_get_cstr(remote->path);
+
+    FURI_LOG_I(TAG, "Storing file: \'%s\'", path);
+
+    bool success = flipper_format_file_open_always(ff, path) &&
+                   flipper_format_write_header_cstr(ff, XREMOTE_FILE_TYPE, XREMOTE_FILE_VERSION);
+
+    // save Items
+    if(success) {
+        CrossRemoteItemArray_it_t it;
+        for(CrossRemoteItemArray_it(it, remote->items); !CrossRemoteItemArray_end_p(it);
+            CrossRemoteItemArray_next(it)) {
+            CrossRemoteItem* item = *CrossRemoteItemArray_cref(it);
+            success = false;
+            if(item->type == XRemoteRemoteItemTypeInfrared) {
+                success = xremote_cross_remote_item_ir_signal_save(
+                    xremote_cross_remote_item_get_ir_signal(item),
+                    ff,
+                    xremote_cross_remote_item_get_name(item),
+                    xremote_cross_remote_item_get_time(item));
+            } else if(item->type == XRemoteRemoteItemTypePause) {
+                success = xremote_cross_remote_item_pause_save(
+                    ff, item->time, xremote_cross_remote_item_get_name(item));
+            } else if(item->type == XRemoteRemoteItemTypeSubGhz) {
+                success = xremote_cross_remote_item_sg_signal_save(
+                    xremote_cross_remote_item_get_sg_signal(item),
+                    ff,
+                    xremote_cross_remote_item_get_name(item),
+                    xremote_cross_remote_item_get_filename(item));
+            }
+            if(!success) {
+                break;
+            }
+        }
+    }
+
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}
+
+bool xremote_cross_remote_save_new(CrossRemote* remote, const char* name) {
+    FuriString *new_name, *new_path;
+    new_name = furi_string_alloc_set(name);
+    new_path = furi_string_alloc_set(XREMOTE_APP_FOLDER);
+
+    xremote_cross_remote_find_vacant_remote_name(new_name, furi_string_get_cstr(new_path));
+    furi_string_cat_printf(
+        new_path, "/%s%s", furi_string_get_cstr(new_name), XREMOTE_APP_EXTENSION);
+
+    xremote_cross_remote_set_name(remote, furi_string_get_cstr(new_name));
+    xremote_cross_remote_set_path(remote, furi_string_get_cstr(new_path));
+
+    furi_string_free(new_name);
+    furi_string_free(new_path);
+    return xremote_cross_remote_store(remote);
+}
+
+static void xremote_cross_remote_reset(CrossRemote* remote) {
+    furi_string_reset(remote->name);
+    furi_string_reset(remote->path);
+    xremote_cross_remote_clear_items(remote);
+    remote->transmitting = 0;
+}
+
+bool xremote_cross_remote_delete(CrossRemote* remote) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FS_Error status = storage_common_remove(storage, furi_string_get_cstr(remote->path));
+
+    furi_record_close(RECORD_STORAGE);
+    xremote_cross_remote_reset(remote);
+    return (status == FSE_OK || status == FSE_NOT_EXIST);
+}

+ 29 - 0
cross_remote/models/cross/xremote_cross_remote.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include "xremote_cross_remote_item.h"
+#include "../../xremote_i.h"
+
+#define CROSS_REMOTE_PAUSE_NAME "Pause %ds"
+
+CrossRemote* xremote_cross_remote_alloc();
+
+void xremote_cross_remote_free(CrossRemote* cross_remote);
+bool xremote_cross_remote_load(CrossRemote* cross_remote, FuriString* path);
+const char* xremote_cross_remote_get_name(CrossRemote* remote);
+void xremote_cross_remote_set_transmitting(CrossRemote* remote, int status);
+int xremote_cross_remote_get_transmitting(CrossRemote* remote);
+bool xremote_cross_remote_add_pause(CrossRemote* remote, int time);
+bool xremote_cross_remote_add_ir_item(
+    CrossRemote* remote,
+    const char* name,
+    InfraredSignal* signal,
+    uint32_t timing);
+bool xremote_cross_remote_add_subghz(CrossRemote* remote, SubGhzRemote* subghz);
+void xremote_cross_remote_remove_item(CrossRemote* remote, size_t index);
+void xremote_cross_remote_rename_item(CrossRemote* remote, size_t index, const char* name);
+size_t xremote_cross_remote_get_item_count(CrossRemote* remote);
+CrossRemoteItem* xremote_cross_remote_get_item(CrossRemote* remote, size_t index);
+int16_t xremote_cross_remote_get_item_type(CrossRemote* remote, size_t index);
+
+bool xremote_cross_remote_save_new(CrossRemote* remote, const char* name);
+bool xremote_cross_remote_delete(CrossRemote* remote);

+ 290 - 0
cross_remote/models/cross/xremote_cross_remote_item.c

@@ -0,0 +1,290 @@
+#include "xremote_cross_remote_item.h"
+
+CrossRemoteItem* xremote_cross_remote_item_alloc() {
+    CrossRemoteItem* item = malloc(sizeof(CrossRemoteItem));
+    item->name = furi_string_alloc();
+    item->filename = furi_string_alloc();
+    item->time = 0;
+    item->type = 0;
+    item->ir_signal = xremote_ir_signal_alloc();
+    item->sg_signal = xremote_sg_remote_alloc();
+
+    return item;
+}
+
+static inline bool xremote_ir_signal_save_message(InfraredMessage* message, FlipperFormat* ff) {
+    const char* protocol_name = infrared_get_protocol_name(message->protocol);
+    return flipper_format_write_string_cstr(ff, "type", "parsed") &&
+           flipper_format_write_string_cstr(ff, "protocol", protocol_name) &&
+           flipper_format_write_hex(ff, "address", (uint8_t*)&message->address, 4) &&
+           flipper_format_write_hex(ff, "command", (uint8_t*)&message->command, 4);
+}
+
+static inline bool xremote_ir_signal_save_raw(InfraredRawSignal* raw, FlipperFormat* ff) {
+    furi_assert(raw->timings_size <= MAX_TIMINGS_AMOUNT);
+    return flipper_format_write_string_cstr(ff, "type", "raw") &&
+           flipper_format_write_uint32(ff, "frequency", &raw->frequency, 1) &&
+           flipper_format_write_float(ff, "duty_cycle", &raw->duty_cycle, 1) &&
+           flipper_format_write_uint32(ff, "data", raw->timings, raw->timings_size);
+}
+
+static inline bool xremote_sg_signal_save_data(SubGhzRemote* remote, FlipperFormat* ff) {
+    UNUSED(remote);
+    UNUSED(ff);
+    return true;
+}
+
+static bool xremote_ir_signal_is_message_valid(InfraredMessage* message) {
+    if(!infrared_is_protocol_valid(message->protocol)) {
+        FURI_LOG_E(TAG, "Unknown protocol");
+        return false;
+    }
+
+    uint32_t address_length = infrared_get_protocol_address_length(message->protocol);
+    uint32_t address_mask = (1UL << address_length) - 1;
+
+    if(message->address != (message->address & address_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Address is out of range (mask 0x%08lX): 0x%lX\r\n",
+            address_mask,
+            message->address);
+        return false;
+    }
+
+    uint32_t command_length = infrared_get_protocol_command_length(message->protocol);
+    uint32_t command_mask = (1UL << command_length) - 1;
+
+    if(message->command != (message->command & command_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Command is out of range (mask 0x%08lX): 0x%lX\r\n",
+            command_mask,
+            message->command);
+        return false;
+    }
+
+    return true;
+}
+
+static bool xremote_cross_remote_item_read_sg(CrossRemoteItem* item, FlipperFormat* ff) {
+    FuriString* buf;
+    bool success = false;
+    buf = furi_string_alloc();
+    item->type = XRemoteRemoteItemTypeSubGhz;
+    item->time = 0;
+    do {
+        success = flipper_format_read_string(ff, "name", item->name) &&
+                  flipper_format_read_string(ff, "filename", item->filename);
+        if(!success) break;
+
+    } while(false);
+    furi_string_free(buf);
+    FURI_LOG_D(TAG, "final name: %s", furi_string_get_cstr(item->name));
+    FURI_LOG_D(TAG, "final filename: %s", furi_string_get_cstr(item->filename));
+
+    return success;
+}
+
+static bool
+    xremote_cross_remote_item_read_ir_signal_raw(CrossRemoteItem* item, FlipperFormat* ff) {
+    uint32_t timings_size, frequency;
+    float duty_cycle;
+
+    bool success = flipper_format_read_uint32(ff, "frequency", &frequency, 1) &&
+                   flipper_format_read_float(ff, "duty_cycle", &duty_cycle, 1) &&
+                   flipper_format_get_value_count(ff, "data", &timings_size);
+
+    if(!success || timings_size > MAX_TIMINGS_AMOUNT) {
+        return false;
+    }
+
+    uint32_t* timings = malloc(sizeof(uint32_t) * timings_size);
+    success = flipper_format_read_uint32(ff, "data", timings, timings_size);
+
+    if(success) {
+        xremote_ir_signal_set_raw_signal(
+            item->ir_signal, timings, timings_size, frequency, duty_cycle);
+    }
+
+    free(timings);
+
+    return success;
+}
+
+static bool xremote_cross_remote_item_read_message(CrossRemoteItem* item, FlipperFormat* ff) {
+    FuriString* buf = furi_string_alloc();
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "protocol", buf)) break;
+        InfraredMessage message;
+        message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
+        success = flipper_format_read_hex(ff, "address", (uint8_t*)&message.address, 4) &&
+                  flipper_format_read_hex(ff, "command", (uint8_t*)&message.command, 4) &&
+                  xremote_ir_signal_is_message_valid(&message);
+
+        if(!success) break;
+
+        xremote_ir_signal_set_message(item->ir_signal, &message);
+    } while(false);
+
+    furi_string_free(buf);
+    return success;
+}
+
+static bool xremote_cross_remote_item_read_ir(CrossRemoteItem* item, FlipperFormat* ff) {
+    FuriString* buf;
+    bool success = false;
+    buf = furi_string_alloc();
+    item->type = XRemoteRemoteItemTypeInfrared;
+    item->time = 0;
+
+    do {
+        if(!flipper_format_read_string(ff, "name", item->name)) break;
+        if(!flipper_format_read_uint32(ff, "time", &item->time, 1)) item->time = 1000;
+        if(!flipper_format_read_string(ff, "type", buf)) break;
+        if(furi_string_equal(buf, "raw")) {
+            if(!xremote_cross_remote_item_read_ir_signal_raw(item, ff)) break;
+        } else if(furi_string_equal(buf, "parsed")) {
+            if(!xremote_cross_remote_item_read_message(item, ff)) break;
+        } else {
+            break;
+        }
+        success = true;
+    } while(false);
+    furi_string_free(buf);
+
+    return success;
+}
+
+static bool xremote_cross_remote_item_read_pause(CrossRemoteItem* item, FlipperFormat* ff) {
+    bool success = false;
+    item->type = XRemoteRemoteItemTypePause;
+
+    do {
+        if(!flipper_format_read_string(ff, "name", item->name)) break;
+        if(!flipper_format_read_uint32(ff, "time", &item->time, 1)) break;
+        success = true;
+    } while(false);
+
+    return success;
+}
+
+bool xremote_cross_remote_item_read(CrossRemoteItem* item, FlipperFormat* ff) {
+    FuriString* type = furi_string_alloc();
+    bool success = false;
+    do {
+        if(!flipper_format_read_string(ff, "remote_type", type)) break;
+        if(furi_string_equal(type, "IR")) {
+            success = xremote_cross_remote_item_read_ir(item, ff);
+        } else if(furi_string_equal(type, "SG")) {
+            success = xremote_cross_remote_item_read_sg(item, ff);
+        } else if(furi_string_equal(type, "PAUSE")) {
+            success = xremote_cross_remote_item_read_pause(item, ff);
+        } else {
+            break;
+        }
+        success = true;
+    } while(false);
+    furi_string_free(type);
+    return success;
+}
+
+void xremote_cross_remote_item_free(CrossRemoteItem* item) {
+    furi_string_free(item->name);
+    furi_string_free(item->filename);
+    //Determine type before free
+    xremote_ir_signal_free(item->ir_signal);
+    xremote_sg_remote_free(item->sg_signal);
+    free(item);
+}
+
+void xremote_cross_remote_item_set_type(CrossRemoteItem* item, int type) {
+    item->type = type;
+}
+
+void xremote_cross_remote_item_set_name(CrossRemoteItem* item, const char* name) {
+    furi_string_set(item->name, name);
+}
+
+void xremote_cross_remote_item_set_filename(CrossRemoteItem* item, const char* filename) {
+    furi_string_set(item->filename, filename);
+}
+
+void xremote_cross_remote_item_set_time(CrossRemoteItem* item, uint32_t time) {
+    item->time = time;
+}
+
+void xremote_cross_remote_item_set_ir_signal(CrossRemoteItem* item, InfraredSignal* signal) {
+    xremote_ir_signal_set_signal(item->ir_signal, signal);
+}
+
+void xremote_cross_remote_item_set_sg_signal(CrossRemoteItem* item, SubGhzRemote* subghz) {
+    item->sg_signal = subghz;
+}
+
+int16_t xremote_cross_remote_item_get_type(CrossRemoteItem* item) {
+    return item->type;
+}
+
+const char* xremote_cross_remote_item_get_name(CrossRemoteItem* item) {
+    return furi_string_get_cstr(item->name);
+}
+
+const char* xremote_cross_remote_item_get_filename(CrossRemoteItem* item) {
+    return furi_string_get_cstr(item->filename);
+}
+
+InfraredSignal* xremote_cross_remote_item_get_ir_signal(CrossRemoteItem* item) {
+    return item->ir_signal;
+}
+
+SubGhzRemote* xremote_cross_remote_item_get_sg_signal(CrossRemoteItem* item) {
+    return item->sg_signal;
+}
+
+uint32_t xremote_cross_remote_item_get_time(CrossRemoteItem* item) {
+    return item->time;
+}
+
+bool xremote_cross_remote_item_ir_signal_save(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const char* name,
+    uint32_t time) {
+    if(!flipper_format_write_comment_cstr(ff, "") ||
+       !flipper_format_write_string_cstr(ff, "remote_type", "IR") ||
+       !flipper_format_write_string_cstr(ff, "name", name) ||
+       !flipper_format_write_uint32(ff, "time", &time, 1)) {
+        return false;
+    } else if(signal->is_raw) {
+        return xremote_ir_signal_save_raw(&signal->payload.raw, ff);
+    } else {
+        return xremote_ir_signal_save_message(&signal->payload.message, ff);
+    }
+}
+
+bool xremote_cross_remote_item_pause_save(FlipperFormat* ff, uint32_t time, const char* name) {
+    if(!flipper_format_write_comment_cstr(ff, "") ||
+       !flipper_format_write_string_cstr(ff, "remote_type", "PAUSE") ||
+       !flipper_format_write_string_cstr(ff, "name", name) ||
+       !flipper_format_write_uint32(ff, "time", &time, 1)) {
+        return false;
+    }
+    return true;
+}
+
+bool xremote_cross_remote_item_sg_signal_save(
+    SubGhzRemote* remote,
+    FlipperFormat* ff,
+    const char* name,
+    const char* filename) {
+    if(!flipper_format_write_comment_cstr(ff, "") ||
+       !flipper_format_write_string_cstr(ff, "remote_type", "SG") ||
+       !flipper_format_write_string_cstr(ff, "name", name) ||
+       !flipper_format_write_string_cstr(ff, "filename", filename)) {
+        return false;
+    }
+    return xremote_sg_signal_save_data(remote, ff);
+}

+ 37 - 0
cross_remote/models/cross/xremote_cross_remote_item.h

@@ -0,0 +1,37 @@
+#pragma once
+
+#include "../infrared/xremote_ir_signal.h"
+#include "../subghz/xremote_sg_remote.h"
+#include "../../xremote_i.h"
+
+bool xremote_cross_remote_item_read(CrossRemoteItem* item, FlipperFormat* ff);
+
+CrossRemoteItem* xremote_cross_remote_item_alloc();
+void xremote_cross_remote_item_free(CrossRemoteItem* item);
+
+void xremote_cross_remote_item_set_name(CrossRemoteItem* item, const char* name);
+const char* xremote_cross_remote_item_get_name(CrossRemoteItem* item);
+void xremote_cross_remote_item_set_filename(CrossRemoteItem* item, const char* filename);
+const char* xremote_cross_remote_item_get_filename(CrossRemoteItem* item);
+
+void xremote_cross_remote_item_set_type(CrossRemoteItem* item, int type);
+int16_t xremote_cross_remote_item_get_type(CrossRemoteItem* item);
+void xremote_cross_remote_item_set_time(CrossRemoteItem* item, uint32_t time);
+uint32_t xremote_cross_remote_item_get_time(CrossRemoteItem* item);
+
+InfraredSignal* xremote_cross_remote_item_get_ir_signal(CrossRemoteItem* item);
+void xremote_cross_remote_item_set_ir_signal(CrossRemoteItem* item, InfraredSignal* signal);
+SubGhzRemote* xremote_cross_remote_item_get_sg_signal(CrossRemoteItem* item);
+void xremote_cross_remote_item_set_sg_signal(CrossRemoteItem* item, SubGhzRemote* subghz);
+
+bool xremote_cross_remote_item_pause_save(FlipperFormat* ff, uint32_t time, const char* name);
+bool xremote_cross_remote_item_ir_signal_save(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const char* name,
+    uint32_t time);
+bool xremote_cross_remote_item_sg_signal_save(
+    SubGhzRemote* remote,
+    FlipperFormat* ff,
+    const char* name,
+    const char* filename);

+ 91 - 0
cross_remote/models/infrared/xremote_ir_remote.c

@@ -0,0 +1,91 @@
+#include "xremote_ir_remote.h"
+
+#define TAG "XremoteInfraredRemote"
+
+ARRAY_DEF(InfraredButtonArray, InfraredRemoteButton*, M_PTR_OPLIST);
+
+struct InfraredRemote {
+    InfraredButtonArray_t buttons;
+    FuriString* name;
+    FuriString* path;
+};
+
+InfraredRemote* xremote_ir_remote_alloc() {
+    InfraredRemote* remote = malloc(sizeof(InfraredRemote));
+    InfraredButtonArray_init(remote->buttons);
+    remote->name = furi_string_alloc();
+    remote->path = furi_string_alloc();
+    return remote;
+}
+
+const char* xremote_ir_remote_get_name(InfraredRemote* remote) {
+    return furi_string_get_cstr(remote->name);
+}
+
+static void xremote_ir_remote_clear_buttons(InfraredRemote* remote) {
+    InfraredButtonArray_it_t it;
+    for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it);
+        InfraredButtonArray_next(it)) {
+        xremote_ir_remote_button_free(*InfraredButtonArray_cref(it));
+    }
+    InfraredButtonArray_reset(remote->buttons);
+}
+
+void xremote_ir_remote_free(InfraredRemote* remote) {
+    furi_string_free(remote->path);
+    furi_string_free(remote->name);
+    free(remote);
+}
+
+InfraredRemoteButton* xremote_ir_get_button(InfraredRemote* remote, size_t index) {
+    furi_assert(index < InfraredButtonArray_size(remote->buttons));
+    return *InfraredButtonArray_get(remote->buttons, index);
+}
+
+bool xremote_ir_remote_load(InfraredRemote* remote, FuriString* path) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+
+    FuriString* buf;
+    buf = furi_string_alloc();
+
+    FURI_LOG_I(TAG, "loading IR Remote: \'%s\'", furi_string_get_cstr(path));
+    bool success = false;
+
+    do {
+        if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path))) break;
+        uint32_t version;
+        if(!flipper_format_read_header(ff, buf, &version)) break;
+        if(!furi_string_equal(buf, "IR signals file") || (version != 1)) break;
+
+        path_extract_filename(path, buf, true);
+        xremote_ir_remote_clear_buttons(remote);
+
+        for(bool can_read = true; can_read;) {
+            InfraredRemoteButton* button = xremote_ir_remote_button_alloc();
+            can_read =
+                xremote_ir_signal_read(xremote_ir_remote_button_get_signal(button), ff, buf);
+            if(can_read) {
+                xremote_ir_remote_button_set_name(button, furi_string_get_cstr(buf));
+                InfraredButtonArray_push_back(remote->buttons, button);
+            } else {
+                xremote_ir_remote_button_free(button);
+            }
+        }
+        success = true;
+    } while(false);
+
+    furi_string_free(buf);
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}
+
+size_t xremote_ir_remote_get_button_count(InfraredRemote* remote) {
+    return InfraredButtonArray_size(remote->buttons);
+}
+
+InfraredRemoteButton* xremote_ir_remote_get_button(InfraredRemote* remote, size_t index) {
+    furi_assert(index < InfraredButtonArray_size(remote->buttons));
+    return *InfraredButtonArray_get(remote->buttons, index);
+}

+ 22 - 0
cross_remote/models/infrared/xremote_ir_remote.h

@@ -0,0 +1,22 @@
+#pragma once
+
+//#include "../../xremote_i.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <m-array.h>
+#include <toolbox/path.h>
+#include <storage/storage.h>
+#include <core/common_defines.h>
+#include "xremote_ir_remote_button.h"
+
+typedef struct InfraredRemote InfraredRemote;
+
+InfraredRemote* xremote_ir_remote_alloc();
+void xremote_ir_remote_free(InfraredRemote* remote);
+
+const char* xremote_ir_remote_get_name(InfraredRemote* remote);
+
+bool xremote_ir_remote_load(InfraredRemote* remote, FuriString* path);
+size_t xremote_ir_remote_get_button_count(InfraredRemote* remote);
+InfraredRemoteButton* xremote_ir_remote_get_button(InfraredRemote* remote, size_t index);

+ 33 - 0
cross_remote/models/infrared/xremote_ir_remote_button.c

@@ -0,0 +1,33 @@
+#include "xremote_ir_remote_button.h"
+
+#include <stdlib.h>
+
+struct InfraredRemoteButton {
+    FuriString* name;
+    InfraredSignal* signal;
+};
+
+InfraredRemoteButton* xremote_ir_remote_button_alloc() {
+    InfraredRemoteButton* button = malloc(sizeof(InfraredRemoteButton));
+    button->name = furi_string_alloc();
+    button->signal = xremote_ir_signal_alloc();
+    return button;
+}
+
+void xremote_ir_remote_button_free(InfraredRemoteButton* button) {
+    furi_string_free(button->name);
+    xremote_ir_signal_free(button->signal);
+    free(button);
+}
+
+void xremote_ir_remote_button_set_name(InfraredRemoteButton* button, const char* name) {
+    furi_string_set(button->name, name);
+}
+
+const char* xremote_ir_remote_button_get_name(InfraredRemoteButton* button) {
+    return furi_string_get_cstr(button->name);
+}
+
+InfraredSignal* xremote_ir_remote_button_get_signal(InfraredRemoteButton* button) {
+    return button->signal;
+}

+ 13 - 0
cross_remote/models/infrared/xremote_ir_remote_button.h

@@ -0,0 +1,13 @@
+#pragma once
+
+//#include "../../xremote_i.h"
+#include "xremote_ir_signal.h"
+//#include "../xremote.h"
+
+typedef struct InfraredRemoteButton InfraredRemoteButton;
+
+InfraredRemoteButton* xremote_ir_remote_button_alloc();
+void xremote_ir_remote_button_free(InfraredRemoteButton* button);
+void xremote_ir_remote_button_set_name(InfraredRemoteButton* button, const char* name);
+const char* xremote_ir_remote_button_get_name(InfraredRemoteButton* button);
+InfraredSignal* xremote_ir_remote_button_get_signal(InfraredRemoteButton* button);

+ 218 - 0
cross_remote/models/infrared/xremote_ir_signal.c

@@ -0,0 +1,218 @@
+#include "xremote_ir_signal.h"
+#include "../../xremote_i.h"
+
+static void xremote_ir_signal_clear_timings(InfraredSignal* signal) {
+    if(signal->is_raw) {
+        free(signal->payload.raw.timings);
+        signal->payload.raw.timings_size = 0;
+        signal->payload.raw.timings = NULL;
+    }
+}
+
+static bool xremote_ir_signal_is_message_valid(InfraredMessage* message) {
+    if(!infrared_is_protocol_valid(message->protocol)) {
+        FURI_LOG_E(TAG, "Unknown protocol");
+        return false;
+    }
+
+    uint32_t address_length = infrared_get_protocol_address_length(message->protocol);
+    uint32_t address_mask = (1UL << address_length) - 1;
+
+    if(message->address != (message->address & address_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Address is out of range (mask 0x%08lX): 0x%lX\r\n",
+            address_mask,
+            message->address);
+        return false;
+    }
+
+    uint32_t command_length = infrared_get_protocol_command_length(message->protocol);
+    uint32_t command_mask = (1UL << command_length) - 1;
+
+    if(message->command != (message->command & command_mask)) {
+        FURI_LOG_E(
+            TAG,
+            "Command is out of range (mask 0x%08lX): 0x%lX\r\n",
+            command_mask,
+            message->command);
+        return false;
+    }
+
+    return true;
+}
+
+static bool xremote_ir_signal_is_raw_valid(InfraredRawSignal* raw) {
+    if((raw->frequency > INFRARED_MAX_FREQUENCY) || (raw->frequency < INFRARED_MIN_FREQUENCY)) {
+        FURI_LOG_E(
+            TAG,
+            "Frequency is out of range (%X - %X): %lX",
+            INFRARED_MIN_FREQUENCY,
+            INFRARED_MAX_FREQUENCY,
+            raw->frequency);
+        return false;
+
+    } else if((raw->duty_cycle <= 0) || (raw->duty_cycle > 1)) {
+        FURI_LOG_E(TAG, "Duty cycle is out of range (0 - 1): %f", (double)raw->duty_cycle);
+        return false;
+
+    } else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) {
+        FURI_LOG_E(
+            TAG,
+            "Timings amount is out of range (0 - %X): %zX",
+            MAX_TIMINGS_AMOUNT,
+            raw->timings_size);
+        return false;
+    }
+
+    return true;
+}
+
+static inline bool xremote_ir_signal_read_message(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* buf;
+    buf = furi_string_alloc();
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "protocol", buf)) break;
+
+        InfraredMessage message;
+        message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
+        success = flipper_format_read_hex(ff, "address", (uint8_t*)&message.address, 4) &&
+                  flipper_format_read_hex(ff, "command", (uint8_t*)&message.command, 4) &&
+                  xremote_ir_signal_is_message_valid(&message);
+
+        if(!success) break;
+
+        xremote_ir_signal_set_message(signal, &message);
+    } while(0);
+
+    return success;
+}
+
+static inline bool xremote_ir_signal_read_raw(InfraredSignal* signal, FlipperFormat* ff) {
+    uint32_t timings_size, frequency;
+    float duty_cycle;
+
+    bool success = flipper_format_read_uint32(ff, "frequency", &frequency, 1) &&
+                   flipper_format_read_float(ff, "duty_cycle", &duty_cycle, 1) &&
+                   flipper_format_get_value_count(ff, "data", &timings_size);
+
+    if(!success || timings_size > MAX_TIMINGS_AMOUNT) {
+        return false;
+    }
+
+    uint32_t* timings = malloc(sizeof(uint32_t) * timings_size);
+    success = flipper_format_read_uint32(ff, "data", timings, timings_size);
+
+    if(success) {
+        xremote_ir_signal_set_raw_signal(signal, timings, timings_size, frequency, duty_cycle);
+    }
+
+    free(timings);
+    return success;
+}
+
+InfraredSignal* xremote_ir_signal_alloc() {
+    InfraredSignal* signal = malloc(sizeof(InfraredSignal));
+
+    signal->is_raw = false;
+    signal->payload.message.protocol = InfraredProtocolUnknown;
+
+    return signal;
+}
+
+void xremote_ir_signal_free(InfraredSignal* signal) {
+    xremote_ir_signal_clear_timings(signal);
+    free(signal);
+}
+
+bool xremote_ir_signal_is_raw(InfraredSignal* signal) {
+    return signal->is_raw;
+}
+
+bool xremote_ir_signal_is_valid(InfraredSignal* signal) {
+    return signal->is_raw ? xremote_ir_signal_is_raw_valid(&signal->payload.raw) :
+                            xremote_ir_signal_is_message_valid(&signal->payload.message);
+}
+
+void xremote_ir_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other) {
+    if(other->is_raw) {
+        const InfraredRawSignal* raw = &other->payload.raw;
+        xremote_ir_signal_set_raw_signal(
+            signal, raw->timings, raw->timings_size, raw->frequency, raw->duty_cycle);
+    } else {
+        const InfraredMessage* message = &other->payload.message;
+        xremote_ir_signal_set_message(signal, message);
+    }
+}
+
+void xremote_ir_signal_set_raw_signal(
+    InfraredSignal* signal,
+    const uint32_t* timings,
+    size_t timings_size,
+    uint32_t frequency,
+    float duty_cycle) {
+    xremote_ir_signal_clear_timings(signal);
+
+    signal->is_raw = true;
+
+    signal->payload.raw.timings_size = timings_size;
+    signal->payload.raw.frequency = frequency;
+    signal->payload.raw.duty_cycle = duty_cycle;
+
+    signal->payload.raw.timings = malloc(timings_size * sizeof(uint32_t));
+    memcpy(signal->payload.raw.timings, timings, timings_size * sizeof(uint32_t));
+}
+InfraredRawSignal* xremote_ir_signal_get_raw_signal(InfraredSignal* signal) {
+    furi_assert(signal->is_raw);
+    return &signal->payload.raw;
+}
+
+static bool xremote_ir_signal_read_body(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "type", tmp)) break;
+        if(furi_string_equal(tmp, "raw")) {
+            success = xremote_ir_signal_read_raw(signal, ff);
+        } else if(furi_string_equal(tmp, "parsed")) {
+            success = xremote_ir_signal_read_message(signal, ff);
+        } else {
+            FURI_LOG_E(TAG, "Unknown signal type");
+        }
+    } while(false);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+bool xremote_ir_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "name", tmp)) break;
+        furi_string_set(name, tmp);
+        if(!xremote_ir_signal_read_body(signal, ff)) break;
+        success = true;
+    } while(0);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+void xremote_ir_signal_set_message(InfraredSignal* signal, const InfraredMessage* message) {
+    xremote_ir_signal_clear_timings(signal);
+
+    signal->is_raw = false;
+    signal->payload.message = *message;
+}
+
+InfraredMessage* xremote_ir_signal_get_message(InfraredSignal* signal) {
+    furi_assert(!signal->is_raw);
+    return &signal->payload.message;
+}

+ 38 - 0
cross_remote/models/infrared/xremote_ir_signal.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <infrared.h>
+#include <flipper_format/flipper_format.h>
+
+typedef struct InfraredSignal InfraredSignal;
+
+typedef struct {
+    size_t timings_size;
+    uint32_t* timings;
+    uint32_t frequency;
+    float duty_cycle;
+} InfraredRawSignal;
+
+InfraredSignal* xremote_ir_signal_alloc();
+void xremote_ir_signal_free(InfraredSignal* signal);
+
+bool xremote_ir_signal_is_raw(InfraredSignal* signal);
+bool xremote_ir_signal_is_valid(InfraredSignal* signal);
+
+void xremote_ir_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other);
+
+void xremote_ir_signal_set_raw_signal(
+    InfraredSignal* signal,
+    const uint32_t* timings,
+    size_t timings_size,
+    uint32_t frequency,
+    float duty_cycle);
+InfraredRawSignal* xremote_ir_signal_get_raw_signal(InfraredSignal* signal);
+
+void xremote_ir_signal_set_message(InfraredSignal* signal, const InfraredMessage* message);
+InfraredMessage* xremote_ir_signal_get_message(InfraredSignal* signal);
+
+bool xremote_ir_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name);

+ 65 - 0
cross_remote/models/subghz/xremote_sg_remote.c

@@ -0,0 +1,65 @@
+#include "xremote_sg_remote.h"
+
+#define TAG "Xremote"
+
+struct SubGhzRemote {
+    FuriString* name;
+    FuriString* filename;
+};
+
+const char* xremote_sg_remote_get_name(SubGhzRemote* remote) {
+    return furi_string_get_cstr(remote->name);
+}
+
+const char* xremote_sg_remote_get_filename(SubGhzRemote* remote) {
+    return furi_string_get_cstr(remote->filename);
+}
+
+SubGhzRemote* xremote_sg_remote_alloc() {
+    SubGhzRemote* remote = malloc(sizeof(SubGhzRemote));
+    remote->name = furi_string_alloc();
+    remote->filename = furi_string_alloc();
+
+    return remote;
+}
+
+void xremote_sg_remote_free(SubGhzRemote* remote) {
+    furi_string_free(remote->name);
+    furi_string_free(remote->filename);
+
+    free(remote);
+}
+
+bool xremote_sg_remote_load(SubGhzRemote* remote, FuriString* path) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+
+    FuriString* buf;
+    buf = furi_string_alloc();
+
+    FURI_LOG_I(TAG, "loading SG Remote: \'%s\'", furi_string_get_cstr(path));
+    bool success = false;
+
+    do {
+        if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path))) break;
+        const char* fullPath = furi_string_get_cstr(path);
+        char* fileName = strrchr(fullPath, '/') + 1;
+        furi_string_set_str(remote->filename, fullPath);
+        char* dotPosition = strrchr(fileName, '.');
+        if(dotPosition != NULL) { // check if there is a dot in the file name
+            *dotPosition = '\0'; // set the dot position to NULL character to truncate the string
+        }
+        furi_string_set_str(remote->name, fileName);
+        uint32_t version;
+        if(!flipper_format_read_header(ff, buf, &version)) break;
+        if(!furi_string_equal(buf, "Flipper SubGhz RAW File") || (version != 1)) break;
+
+        success = true;
+    } while(false);
+
+    furi_string_free(buf);
+    flipper_format_buffered_file_close(ff);
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}

+ 22 - 0
cross_remote/models/subghz/xremote_sg_remote.h

@@ -0,0 +1,22 @@
+#pragma once
+
+//#include "../../xremote_i.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+//#include <m-array.h>
+#include <storage/storage.h>
+#include <core/common_defines.h>
+#include <flipper_format/flipper_format.h>
+#include <flipper_format/flipper_format_i.h>
+
+typedef struct SubGhzRemote SubGhzRemote;
+
+const char* xremote_sg_remote_get_name(SubGhzRemote* remote);
+const char* xremote_sg_remote_get_filename(SubGhzRemote* remote);
+
+SubGhzRemote* xremote_sg_remote_alloc();
+
+void xremote_sg_remote_free(SubGhzRemote* remote);
+
+bool xremote_sg_remote_load(SubGhzRemote* remote, FuriString* path);

+ 30 - 0
cross_remote/scenes/xremote_scene.c

@@ -0,0 +1,30 @@
+#include "xremote_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const xremote_on_enter_handlers[])(void*) = {
+#include "xremote_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const xremote_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "xremote_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const xremote_on_exit_handlers[])(void* context) = {
+#include "xremote_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers xremote_scene_handlers = {
+    .on_enter_handlers = xremote_on_enter_handlers,
+    .on_event_handlers = xremote_on_event_handlers,
+    .on_exit_handlers = xremote_on_exit_handlers,
+    .scene_num = XRemoteSceneNum,
+};

+ 29 - 0
cross_remote/scenes/xremote_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) XRemoteScene##id,
+typedef enum {
+#include "xremote_scene_config.h"
+    XRemoteSceneNum,
+} XRemoteScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers xremote_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "xremote_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "xremote_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "xremote_scene_config.h"
+#undef ADD_SCENE

+ 18 - 0
cross_remote/scenes/xremote_scene_config.h

@@ -0,0 +1,18 @@
+ADD_SCENE(xremote, infoscreen, Infoscreen)
+ADD_SCENE(xremote, menu, Menu)
+ADD_SCENE(xremote, create, Create)
+ADD_SCENE(xremote, create_add, CreateAdd)
+ADD_SCENE(xremote, edit_item, EditItem)
+ADD_SCENE(xremote, settings, Settings)
+ADD_SCENE(xremote, wip, Wip)
+ADD_SCENE(xremote, ir_list, IrList)
+ADD_SCENE(xremote, xr_list, XrList)
+ADD_SCENE(xremote, xr_list_edit, XrListEdit)
+ADD_SCENE(xremote, xr_list_edit_item, XrListEditItem)
+ADD_SCENE(xremote, sg_list, SgList)
+ADD_SCENE(xremote, ir_remote, IrRemote)
+ADD_SCENE(xremote, save_remote, SaveRemote)
+ADD_SCENE(xremote, save_remote_item, SaveRemoteItem)
+ADD_SCENE(xremote, transmit, Transmit)
+ADD_SCENE(xremote, pause_set, PauseSet)
+ADD_SCENE(xremote, ir_timer, IrTimer)

+ 103 - 0
cross_remote/scenes/xremote_scene_create.c

@@ -0,0 +1,103 @@
+#include "../xremote.h"
+#include "../helpers/xremote_custom_event.h"
+
+typedef enum {
+    ButtonIndexPlus = -2,
+    ButtonIndexSave = -1,
+    ButtonIndexNA = 0,
+} ButtonIndex;
+
+static void xremote_create_callback(void* context, int32_t index, InputType type) {
+    XRemote* app = context;
+
+    uint16_t custom_type;
+    if(type == InputTypePress) {
+        custom_type = XRemoteCustomEventMenuSelected;
+    } else if(type == InputTypeRelease) {
+        custom_type = XRemoteCustomEventMenuVoid;
+    } else if(type == InputTypeShort) {
+        custom_type = XRemoteCustomEventMenuSelected;
+    } else {
+        furi_crash("Unexpected Input Type");
+    }
+
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, xremote_custom_menu_event_pack(custom_type, index));
+}
+
+void xremote_scene_create_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    ButtonMenu* button_menu = app->button_menu_create;
+
+    size_t item_count = xremote_cross_remote_get_item_count(app->cross_remote);
+    for(size_t i = 0; i < item_count; ++i) {
+        CrossRemoteItem* item = xremote_cross_remote_get_item(app->cross_remote, i);
+        button_menu_add_item(
+            button_menu,
+            xremote_cross_remote_item_get_name(item),
+            i,
+            xremote_create_callback,
+            ButtonMenuItemTypeCommon,
+            context);
+    }
+
+    button_menu_add_item(
+        button_menu,
+        "+",
+        ButtonIndexPlus,
+        xremote_create_callback,
+        ButtonMenuItemTypeControl,
+        context);
+
+    button_menu_add_item(
+        button_menu,
+        "Save",
+        ButtonIndexSave,
+        xremote_create_callback,
+        ButtonMenuItemTypeControl,
+        context);
+
+    button_menu_set_header(button_menu, "Add Cmd");
+    const int16_t button_index =
+        (signed)scene_manager_get_scene_state(app->scene_manager, XRemoteViewIdCreate);
+    button_menu_set_selected_item(button_menu, button_index);
+    scene_manager_set_scene_state(app->scene_manager, XRemoteSceneCreate, ButtonIndexNA);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdCreate);
+}
+
+bool xremote_scene_create_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_next_scene(app->scene_manager, XRemoteSceneMenu);
+        consumed = true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        const uint16_t custom_type = xremote_custom_menu_event_get_type(event.event);
+        const int16_t button_index = xremote_custom_menu_event_get_value(event.event);
+        scene_manager_set_scene_state(
+            app->scene_manager, XRemoteSceneCreate, (unsigned)button_index);
+        if(custom_type == XRemoteCustomEventMenuSelected && button_index < 0) {
+            //scene_manager_set_scene_state(
+            //    app->scene_manager, XRemoteSceneCreate, (unsigned)button_index);
+            if(button_index == ButtonIndexPlus) {
+                scene_manager_next_scene(app->scene_manager, XRemoteSceneCreateAdd);
+                consumed = true;
+            } else if(button_index == ButtonIndexSave) {
+                scene_manager_next_scene(app->scene_manager, XRemoteSceneSaveRemote);
+            }
+        } else if(custom_type == XRemoteCustomEventMenuSelected) {
+            app->edit_item = button_index;
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneEditItem);
+        }
+    }
+
+    return consumed;
+}
+
+void xremote_scene_create_on_exit(void* context) {
+    XRemote* app = context;
+    button_menu_reset(app->button_menu_create);
+}

+ 98 - 0
cross_remote/scenes/xremote_scene_create_add.c

@@ -0,0 +1,98 @@
+#include "../xremote.h"
+#include "../helpers/xremote_custom_event.h"
+
+typedef enum {
+    ButtonIndexIr = -3,
+    ButtonIndexSubghz = -2,
+    ButtonIndexPause = -1,
+    ButtonIndexNA = 0,
+} ButtonIndex;
+
+static void xremote_create_add_callback(void* context, int32_t index, InputType type) {
+    XRemote* app = context;
+
+    uint16_t custom_type;
+    if(type == InputTypePress) {
+        custom_type = XRemoteCustomEventMenuVoid;
+    } else if(type == InputTypeRelease) {
+        custom_type = XRemoteCustomEventMenuAddSelected;
+    } else if(type == InputTypeShort) {
+        custom_type = XRemoteCustomEventMenuVoid;
+    } else {
+        furi_crash("Unexpected Input Type");
+    }
+
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, xremote_custom_menu_event_pack(custom_type, index));
+}
+
+void xremote_scene_create_add_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    ButtonMenu* button_menu = app->button_menu_create_add;
+    SceneManager* scene_manager = app->scene_manager;
+
+    button_menu_add_item(
+        button_menu,
+        "Infrared",
+        ButtonIndexIr,
+        xremote_create_add_callback,
+        ButtonMenuItemTypeCommon,
+        context);
+
+    button_menu_add_item(
+        button_menu,
+        "SubGhz",
+        ButtonIndexSubghz,
+        xremote_create_add_callback,
+        ButtonMenuItemTypeCommon,
+        context);
+
+    button_menu_add_item(
+        button_menu,
+        "Pause",
+        ButtonIndexPause,
+        xremote_create_add_callback,
+        ButtonMenuItemTypeCommon,
+        context);
+
+    button_menu_set_header(button_menu, "Choose Type");
+    const int16_t button_index =
+        (signed)scene_manager_get_scene_state(scene_manager, XRemoteViewIdCreateAdd);
+    button_menu_set_selected_item(button_menu, button_index);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdCreateAdd);
+}
+
+bool xremote_scene_create_add_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        const uint16_t custom_type = xremote_custom_menu_event_get_type(event.event);
+        const int16_t button_index = xremote_custom_menu_event_get_value(event.event);
+        if(custom_type == XRemoteCustomEventMenuAddSelected) {
+            furi_assert(button_index < 0);
+            scene_manager_set_scene_state(
+                app->scene_manager, XRemoteSceneCreate, (unsigned)button_index);
+            if(button_index == ButtonIndexIr) {
+                scene_manager_next_scene(app->scene_manager, XRemoteSceneIrList);
+            }
+            if(button_index == ButtonIndexSubghz) {
+                scene_manager_next_scene(app->scene_manager, XRemoteSceneSgList);
+                //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+            }
+            if(button_index == ButtonIndexPause) {
+                scene_manager_next_scene(app->scene_manager, XRemoteScenePauseSet);
+            }
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void xremote_scene_create_add_on_exit(void* context) {
+    XRemote* app = context;
+    button_menu_reset(app->button_menu_create_add);
+}

+ 64 - 0
cross_remote/scenes/xremote_scene_edit_item.c

@@ -0,0 +1,64 @@
+#include "../xremote.h"
+#include "../models/cross/xremote_cross_remote.h"
+
+enum SubmenuIndexEdit {
+    SubmenuIndexRename = 10,
+    SubmenuIndexTiming,
+    SubmenuIndexDelete,
+};
+
+void xremote_scene_edit_item_submenu_callback(void* context, uint32_t index) {
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void xremote_scene_edit_item_on_enter(void* context) {
+    XRemote* app = context;
+    submenu_add_item(
+        app->editmenu, "Rename", SubmenuIndexRename, xremote_scene_edit_item_submenu_callback, app);
+
+    if(xremote_cross_remote_get_item_type(app->cross_remote, app->edit_item) ==
+       XRemoteRemoteItemTypeInfrared) {
+        submenu_add_item(
+            app->editmenu,
+            "Set Timing",
+            SubmenuIndexTiming,
+            xremote_scene_edit_item_submenu_callback,
+            app);
+    }
+
+    submenu_add_item(
+        app->editmenu, "Delete", SubmenuIndexDelete, xremote_scene_edit_item_submenu_callback, app);
+
+    submenu_set_selected_item(
+        app->editmenu, scene_manager_get_scene_state(app->scene_manager, XRemoteSceneMenu));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdEditItem);
+}
+
+bool xremote_scene_edit_item_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    if(event.type == SceneManagerEventTypeBack) {
+        //exit app
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexDelete) {
+            xremote_cross_remote_remove_item(app->cross_remote, app->edit_item);
+        } else if(event.event == SubmenuIndexRename) {
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneSaveRemoteItem);
+            //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+            return 0;
+        } else if(event.event == SubmenuIndexTiming) {
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneIrTimer);
+            return 0;
+        }
+        scene_manager_next_scene(app->scene_manager, XRemoteSceneCreate);
+    }
+    return 0;
+}
+
+void xremote_scene_edit_item_on_exit(void* context) {
+    XRemote* app = context;
+    submenu_reset(app->editmenu);
+}

+ 55 - 0
cross_remote/scenes/xremote_scene_infoscreen.c

@@ -0,0 +1,55 @@
+#include "../xremote.h"
+#include "../helpers/xremote_custom_event.h"
+#include "../views/xremote_infoscreen.h"
+
+void xremote_scene_infoscreen_callback(XRemoteCustomEvent event, void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void xremote_scene_infoscreen_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    xremote_infoscreen_set_callback(
+        app->xremote_infoscreen, xremote_scene_infoscreen_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdInfoscreen);
+}
+
+bool xremote_scene_infoscreen_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case XRemoteCustomEventInfoscreenLeft:
+        case XRemoteCustomEventInfoscreenRight:
+            break;
+        case XRemoteCustomEventInfoscreenUp:
+        case XRemoteCustomEventInfoscreenDown:
+            break;
+        case XRemoteCustomEventInfoscreenOk:
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneMenu);
+            consumed = true;
+            break;
+        case XRemoteCustomEventInfoscreenBack:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, XRemoteSceneInfoscreen)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void xremote_scene_infoscreen_on_exit(void* context) {
+    XRemote* app = context;
+    UNUSED(app);
+}

+ 42 - 0
cross_remote/scenes/xremote_scene_ir_list.c

@@ -0,0 +1,42 @@
+#include "../xremote.h"
+
+void xremote_scene_ir_list_on_enter(void* context) {
+    XRemote* app = context;
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, INFRARED_APP_EXTENSION, &I_ir_10px);
+    browser_options.base_path = INFRARED_APP_FOLDER;
+
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, INFRARED_APP_FOLDER);
+    bool success = dialog_file_browser_show(app->dialogs, app->file_path, path, &browser_options);
+    furi_string_free(path);
+
+    if(success) {
+        //Load Remote into buffer
+        view_set_orientation(view_stack_get_view(app->view_stack), ViewOrientationVertical);
+        view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdStack);
+
+        xremote_ir_remote_load(app->ir_remote_buffer, app->file_path);
+    }
+
+    if(success) {
+        //Load Remote Button View
+        scene_manager_next_scene(app->scene_manager, XRemoteSceneIrRemote);
+    } else {
+        scene_manager_previous_scene(app->scene_manager);
+    }
+}
+
+bool xremote_scene_ir_list_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    bool consumed = true;
+
+    return consumed;
+}
+
+void xremote_scene_ir_list_on_exit(void* context) {
+    UNUSED(context);
+}

+ 81 - 0
cross_remote/scenes/xremote_scene_ir_remote.c

@@ -0,0 +1,81 @@
+#include "../xremote.h"
+
+typedef enum {
+    ButtonIndexPlus = -2,
+    ButtonIndexEdit = -1,
+    ButtonIndexNA = 0,
+} ButtonIndex;
+
+static void
+    xremote_scene_ir_remote_button_menu_callback(void* context, int32_t index, InputType type) {
+    XRemote* app = context;
+
+    uint16_t custom_type;
+    if(type == InputTypePress) {
+        custom_type = XRemoteCustomEventMenuVoid;
+    } else if(type == InputTypeRelease) {
+        custom_type = XRemoteCustomEventMenuAddIrSelected;
+    } else if(type == InputTypeShort) {
+        custom_type = XRemoteCustomEventMenuVoid;
+    } else {
+        furi_crash("Unexpected IR input type");
+    }
+
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, xremote_custom_menu_event_pack(custom_type, index));
+}
+
+void xremote_scene_ir_remote_on_enter(void* context) {
+    XRemote* app = context;
+    ButtonMenu* button_menu = app->button_menu_ir;
+
+    size_t button_count = xremote_ir_remote_get_button_count(app->ir_remote_buffer);
+    for(size_t i = 0; i < button_count; i++) {
+        InfraredRemoteButton* button = xremote_ir_remote_get_button(app->ir_remote_buffer, i);
+        button_menu_add_item(
+            button_menu,
+            xremote_ir_remote_button_get_name(button),
+            i,
+            xremote_scene_ir_remote_button_menu_callback,
+            ButtonMenuItemTypeCommon,
+            context);
+    }
+
+    button_menu_set_header(button_menu, "Select Cmd");
+    const int16_t button_index =
+        (signed)scene_manager_get_scene_state(app->scene_manager, XRemoteViewIdIrRemote);
+    button_menu_set_selected_item(button_menu, button_index);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdIrRemote);
+}
+
+bool xremote_scene_ir_remote_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        const uint16_t custom_type = xremote_custom_menu_event_get_type(event.event);
+        const int16_t button_index = xremote_custom_menu_event_get_value(event.event);
+        if(custom_type == XRemoteCustomEventMenuAddIrSelected) {
+            scene_manager_set_scene_state(
+                app->scene_manager, XRemoteSceneIrRemote, (unsigned)button_index);
+
+            InfraredRemoteButton* ir_button =
+                xremote_ir_remote_get_button(app->ir_remote_buffer, button_index);
+            const char* button_name = xremote_ir_remote_button_get_name(ir_button);
+            InfraredSignal* signal = xremote_ir_remote_button_get_signal(ir_button);
+
+            xremote_cross_remote_add_ir_item(
+                app->cross_remote, button_name, signal, app->ir_timing);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneCreate);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void xremote_scene_ir_remote_on_exit(void* context) {
+    XRemote* app = context;
+    button_menu_reset(app->button_menu_ir);
+}

+ 58 - 0
cross_remote/scenes/xremote_scene_ir_timer.c

@@ -0,0 +1,58 @@
+#include "../xremote.h"
+#include "../models/cross/xremote_cross_remote.h"
+
+void xremote_scene_ir_timer_callback(void* context) {
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, XRemoteCustomEventTextInput);
+}
+
+void xremote_scene_ir_timer_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    IntInput* int_input = app->int_input;
+    size_t enter_name_length = 5;
+    char* str = "Transmit in ms (0 - 9999)";
+    const char* constStr = str;
+    CrossRemoteItem* item = xremote_cross_remote_get_item(app->cross_remote, app->edit_item);
+    int_input_set_header_text(int_input, constStr);
+    snprintf(app->text_store[1], 5, "%lu", item->time);
+
+    int_input_set_result_callback(
+        int_input,
+        xremote_scene_ir_timer_callback,
+        context,
+        app->text_store[1],
+        enter_name_length,
+        false);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdIntInput);
+}
+
+bool xremote_scene_ir_timer_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        CrossRemoteItem* item = xremote_cross_remote_get_item(app->cross_remote, app->edit_item);
+        xremote_cross_remote_item_set_time(item, atoi(app->text_store[1]));
+        if(item->time > 9999) {
+            item->time = 9999;
+        }
+        //app->first_station = atoi(app->text_store[0]);
+        /*if(app->first_station > app->max_station) {
+            app->first_station = app->max_station;
+            snprintf(app->text_store[0], 5, "%lu", app->first_station);
+        }*/
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    }
+    return consumed;
+}
+
+void xremote_scene_ir_timer_on_exit(void* context) {
+    XRemote* app = context;
+    UNUSED(app);
+}

+ 87 - 0
cross_remote/scenes/xremote_scene_menu.c

@@ -0,0 +1,87 @@
+#include "../helpers/subghz/subghz.h"
+#include "../xremote.h"
+
+enum SubmenuIndex {
+    SubmenuIndexCreate = 10,
+    SubmenuIndexLoad,
+    SubmenuIndexEdit,
+    SubmenuIndexSettings,
+    SubmenuIndexInfoscreen,
+};
+
+void xremote_scene_menu_submenu_callback(void* context, uint32_t index) {
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void xremote_scene_menu_on_enter(void* context) {
+    XRemote* app = context;
+
+    submenu_add_item(
+        app->submenu,
+        "New Command Chain",
+        SubmenuIndexCreate,
+        xremote_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Run Saved Command",
+        SubmenuIndexLoad,
+        xremote_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Edit / Delete Command",
+        SubmenuIndexEdit,
+        xremote_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu, "Settings", SubmenuIndexSettings, xremote_scene_menu_submenu_callback, app);
+    submenu_add_item(
+        app->submenu, "App Info", SubmenuIndexInfoscreen, xremote_scene_menu_submenu_callback, app);
+
+    submenu_set_selected_item(
+        app->submenu, scene_manager_get_scene_state(app->scene_manager, XRemoteSceneMenu));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdMenu);
+}
+
+bool xremote_scene_menu_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    if(event.type == SceneManagerEventTypeBack) {
+        //exit app
+        scene_manager_stop(app->scene_manager);
+        view_dispatcher_stop(app->view_dispatcher);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexCreate) {
+            scene_manager_set_scene_state(
+                app->scene_manager, XRemoteSceneMenu, SubmenuIndexCreate);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneCreate);
+            return true;
+        } else if(event.event == SubmenuIndexLoad) {
+            scene_manager_set_scene_state(app->scene_manager, XRemoteSceneMenu, SubmenuIndexLoad);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneXrList);
+            return true;
+        } else if(event.event == SubmenuIndexSettings) {
+            scene_manager_set_scene_state(
+                app->scene_manager, XRemoteSceneMenu, SubmenuIndexSettings);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneSettings);
+            return true;
+        } else if(event.event == SubmenuIndexInfoscreen) {
+            scene_manager_set_scene_state(
+                app->scene_manager, XRemoteSceneMenu, SubmenuIndexInfoscreen);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneInfoscreen);
+            return true;
+        } else if(event.event == SubmenuIndexEdit) {
+            scene_manager_set_scene_state(app->scene_manager, XRemoteSceneMenu, SubmenuIndexEdit);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneXrListEdit);
+        }
+    }
+    return false;
+}
+
+void xremote_scene_menu_on_exit(void* context) {
+    XRemote* app = context;
+    submenu_reset(app->submenu);
+}

+ 54 - 0
cross_remote/scenes/xremote_scene_pause_set.c

@@ -0,0 +1,54 @@
+#include "../xremote.h"
+#include "../views/xremote_pause_set.h"
+
+void xremote_scene_pause_set_callback(XRemoteCustomEvent event, void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void xremote_scene_pause_set_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    xremote_pause_set_set_callback(app->xremote_pause_set, xremote_scene_pause_set_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdPauseSet);
+}
+
+bool xremote_scene_pause_set_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case XRemoteCustomEventInfoscreenLeft:
+        case XRemoteCustomEventInfoscreenRight:
+            break;
+        case XRemoteCustomEventInfoscreenUp:
+        case XRemoteCustomEventInfoscreenDown:
+            break;
+        case XRemoteCustomEventInfoscreenOk:
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneMenu);
+            consumed = true;
+            break;
+        case XRemoteCustomEventPauseSetBack:
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, XRemoteSceneCreateAdd)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        case XRemoteCustomEventPauseSetOk:
+            //xremote_cross_remote_add_pause(app->cross_remote, time);
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, XRemoteSceneCreate);
+            consumed = true;
+            break;
+        }
+    }
+    return consumed;
+}
+
+void xremote_scene_pause_set_on_exit(void* context) {
+    UNUSED(context);
+}

+ 74 - 0
cross_remote/scenes/xremote_scene_save_remote.c

@@ -0,0 +1,74 @@
+#include "../xremote.h"
+
+#include <string.h>
+#include <toolbox/path.h>
+
+void xremote_scene_save_remote_on_enter(void* context) {
+    XRemote* app = context;
+    CrossRemote* remote = app->cross_remote;
+    TextInput* text_input = app->text_input;
+    size_t enter_name_length = 0;
+
+    text_input_set_header_text(text_input, "Name the remote");
+    enter_name_length = XREMOTE_MAX_REMOTE_NAME_LENGTH;
+
+    FuriString* folder_path;
+    folder_path = furi_string_alloc();
+
+    //if(furi_string_end_with(app->))
+
+    //A lot missing here
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        furi_string_get_cstr(folder_path),
+        XREMOTE_APP_EXTENSION,
+        xremote_cross_remote_get_name(remote));
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    furi_string_free(folder_path);
+
+    text_input_set_result_callback(
+        text_input,
+        xremote_text_input_callback,
+        context,
+        app->text_store[0],
+        enter_name_length,
+        false);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdTextInput);
+}
+
+bool xremote_scene_save_remote_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    CrossRemote* remote = app->cross_remote;
+    SceneManager* scene_manager = app->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        bool success = false;
+
+        success = xremote_cross_remote_save_new(remote, app->text_store[0]);
+
+        if(success) {
+            scene_manager_next_scene(scene_manager, XRemoteSceneMenu);
+        } else {
+            scene_manager_search_and_switch_to_previous_scene(scene_manager, XRemoteSceneCreate);
+        }
+        consumed = true;
+    }
+    return consumed;
+}
+
+void xremote_scene_save_remote_on_exit(void* context) {
+    XRemote* app = context;
+    TextInput* text_input = app->text_input;
+
+    void* validator_context = text_input_get_validator_callback_context(text_input);
+    text_input_set_validator(text_input, NULL, NULL);
+
+    size_t enter_name_length = XREMOTE_MAX_REMOTE_NAME_LENGTH;
+    strncpy(app->text_store[0], "", enter_name_length);
+    if(validator_context) {
+        validator_is_file_free((ValidatorIsFile*)validator_context);
+    }
+}

+ 45 - 0
cross_remote/scenes/xremote_scene_save_remote_item.c

@@ -0,0 +1,45 @@
+#include "../xremote.h"
+#include "../models/cross/xremote_cross_remote.h"
+
+#include <string.h>
+#include <toolbox/path.h>
+
+void xremote_scene_save_remote_item_on_enter(void* context) {
+    XRemote* app = context;
+    TextInput* text_input = app->text_input;
+
+    text_input_set_header_text(text_input, "Name the Sequence");
+
+    size_t enter_name_length = XREMOTE_MAX_REMOTE_NAME_LENGTH;
+    CrossRemoteItem* item = xremote_cross_remote_get_item(app->cross_remote, app->edit_item);
+    strncpy(app->text_store[0], furi_string_get_cstr(item->name), enter_name_length);
+    text_input_set_result_callback(
+        text_input,
+        xremote_text_input_callback,
+        context,
+        app->text_store[0],
+        enter_name_length,
+        false);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdTextInput);
+}
+
+bool xremote_scene_save_remote_item_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    CrossRemote* remote = app->cross_remote;
+    SceneManager* scene_manager = app->scene_manager;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        xremote_cross_remote_rename_item(remote, app->edit_item, app->text_store[0]);
+        scene_manager_next_scene(scene_manager, XRemoteSceneCreate);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void xremote_scene_save_remote_item_on_exit(void* context) {
+    XRemote* app = context;
+    size_t enter_name_length = XREMOTE_MAX_REMOTE_NAME_LENGTH;
+    strncpy(app->text_store[0], "", enter_name_length);
+}

+ 163 - 0
cross_remote/scenes/xremote_scene_settings.c

@@ -0,0 +1,163 @@
+#include "../xremote.h"
+#include <lib/toolbox/value_index.h>
+/*
+enum SettingsIndex {
+    SettingsIndexHaptic = 10,
+    SettingsIndexValue1,
+    SettingsIndexValue2,
+};*/
+
+/*const char* const haptic_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t haptic_value[2] = {
+    XRemoteHapticOff,
+    XRemoteHapticOn,
+};*/
+
+/*const char* const speaker_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t speaker_value[2] = {
+    XRemoteSpeakerOff,
+    XRemoteSpeakerOn,
+};*/
+
+const char* const led_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t led_value[2] = {
+    XRemoteLedOff,
+    XRemoteLedOn,
+};
+
+const char* const settings_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t settings_value[2] = {
+    XRemoteSettingsOff,
+    XRemoteSettingsOn,
+};
+
+/*static void xremote_scene_settings_set_haptic(VariableItem* item) {
+    XRemote* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, haptic_text[index]);
+    app->haptic = haptic_value[index];
+}*/
+
+/*static void xremote_scene_settings_set_speaker(VariableItem* item) {
+    XRemote* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, speaker_text[index]);
+    app->speaker = speaker_value[index];
+}*/
+
+static void xremote_scene_settings_set_led(VariableItem* item) {
+    XRemote* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, led_text[index]);
+    app->led = led_value[index];
+}
+
+static void xremote_scene_settings_set_save_settings(VariableItem* item) {
+    XRemote* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, settings_text[index]);
+    app->save_settings = settings_value[index];
+}
+
+static void xremote_scene_settings_set_ir_timing(VariableItem* item) {
+    XRemote* app = variable_item_get_context(item);
+    uint32_t index = variable_item_get_current_value_index(item);
+
+    snprintf(app->ir_timing_char, 20, "%lu", (index * 100));
+    variable_item_set_current_value_text(item, app->ir_timing_char);
+    app->ir_timing = (index * 100);
+}
+
+static void xremote_scene_settings_set_sg_timing(VariableItem* item) {
+    XRemote* app = variable_item_get_context(item);
+    uint32_t index = variable_item_get_current_value_index(item);
+
+    snprintf(app->sg_timing_char, 20, "%lu", (index * 100));
+    variable_item_set_current_value_text(item, app->sg_timing_char);
+    app->sg_timing = (index * 100);
+}
+
+void xremote_scene_settings_submenu_callback(void* context, uint32_t index) {
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void xremote_scene_settings_on_enter(void* context) {
+    XRemote* app = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    // Vibro on/off
+    /*    item = variable_item_list_add(
+        app->variable_item_list, "Vibro/Haptic:", 2, xremote_scene_settings_set_haptic, app);
+    value_index = value_index_uint32(app->haptic, haptic_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, haptic_text[value_index]);*/
+
+    // Sound on/off
+    /*   item = variable_item_list_add(
+        app->variable_item_list, "Sound:", 2, xremote_scene_settings_set_speaker, app);
+    value_index = value_index_uint32(app->speaker, speaker_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, speaker_text[value_index]);*/
+
+    // LED Effects on/off
+    item = variable_item_list_add(
+        app->variable_item_list, "LED FX:", 2, xremote_scene_settings_set_led, app);
+    value_index = value_index_uint32(app->led, led_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, led_text[value_index]);
+
+    // Save Settings to File
+    item = variable_item_list_add(
+        app->variable_item_list, "Save Settings", 2, xremote_scene_settings_set_save_settings, app);
+    value_index = value_index_uint32(app->save_settings, settings_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, settings_text[value_index]);
+
+    // Set Infrared Timer
+    item = variable_item_list_add(
+        app->variable_item_list, "IR Time ms", 30, xremote_scene_settings_set_ir_timing, app);
+
+    variable_item_set_current_value_index(item, (uint8_t)(app->ir_timing / 100));
+    snprintf(app->ir_timing_char, 20, "%lu", app->ir_timing);
+    variable_item_set_current_value_text(item, app->ir_timing_char);
+
+    // Set SubGhz Timer
+    item = variable_item_list_add(
+        app->variable_item_list, "SubG. Time ms", 30, xremote_scene_settings_set_sg_timing, app);
+
+    variable_item_set_current_value_index(item, (uint8_t)(app->sg_timing / 100));
+    snprintf(app->sg_timing_char, 20, "%lu", app->sg_timing);
+    variable_item_set_current_value_text(item, app->sg_timing_char);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdSettings);
+}
+
+bool xremote_scene_settings_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    UNUSED(app);
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+    }
+    return consumed;
+}
+
+void xremote_scene_settings_on_exit(void* context) {
+    XRemote* app = context;
+    variable_item_list_set_selected_item(app->variable_item_list, 0);
+    variable_item_list_reset(app->variable_item_list);
+}

+ 43 - 0
cross_remote/scenes/xremote_scene_sg_list.c

@@ -0,0 +1,43 @@
+#include "../xremote.h"
+
+void xremote_scene_sg_list_on_enter(void* context) {
+    XRemote* app = context;
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, SUBGHZ_APP_EXTENSION, &I_sg_10px);
+    browser_options.base_path = SUBGHZ_APP_FOLDER;
+
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, SUBGHZ_APP_FOLDER);
+    bool success = dialog_file_browser_show(app->dialogs, app->file_path, path, &browser_options);
+    furi_string_free(path);
+
+    if(success) {
+        //Load Remote into buffer
+        view_set_orientation(view_stack_get_view(app->view_stack), ViewOrientationVertical);
+        view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdStack);
+
+        xremote_sg_remote_load(app->sg_remote_buffer, app->file_path);
+        xremote_cross_remote_add_subghz(app->cross_remote, app->sg_remote_buffer);
+    }
+
+    if(success) {
+        //Load Remote Button View
+        scene_manager_next_scene(app->scene_manager, XRemoteSceneCreate);
+    } else {
+        scene_manager_previous_scene(app->scene_manager);
+    }
+}
+
+bool xremote_scene_sg_list_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    bool consumed = true;
+
+    return consumed;
+}
+
+void xremote_scene_sg_list_on_exit(void* context) {
+    UNUSED(context);
+}

+ 184 - 0
cross_remote/scenes/xremote_scene_transmit.c

@@ -0,0 +1,184 @@
+#include "../xremote.h"
+#include "../views/xremote_transmit.h"
+#include "../models/infrared/xremote_ir_signal.h"
+#include "../helpers/subghz/subghz.h"
+
+static const NotificationSequence* xremote_notification_sequences[] = {
+    &sequence_success,
+    &sequence_set_only_green_255,
+    &sequence_reset_green,
+    &sequence_solid_yellow,
+    &sequence_reset_rgb,
+    &sequence_blink_start_cyan,
+    &sequence_blink_start_magenta,
+    &sequence_blink_stop,
+    &sequence_blink_start_yellow,
+    &sequence_blink_stop,
+    &sequence_blink_start_blue,
+    &sequence_blink_stop,
+};
+
+void xremote_transmit_callback(XRemoteCustomEvent event, void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void xremote_scene_ir_notification_message(XRemote* app, uint32_t message) {
+    if(app->led == 1) {
+        notification_message(app->notification, xremote_notification_sequences[message]);
+    }
+}
+
+bool xremote_scene_ir_signal_is_raw(InfraredSignal* signal) {
+    if(signal->is_raw) {
+        return true;
+    }
+    return false;
+}
+
+void xremote_scene_transmit_stop_ir_signal(XRemote* app) {
+    if(!app->transmitting) {
+        return;
+    }
+    app->transmitting = false;
+    infrared_worker_tx_stop(app->ir_worker);
+    infrared_worker_tx_set_get_signal_callback(app->ir_worker, NULL, NULL);
+    xremote_scene_ir_notification_message(app, InfraredNotificationMessageBlinkStop);
+}
+
+void xremote_scene_transmit_send_ir_signal(XRemote* app, CrossRemoteItem* item) {
+    InfraredSignal* signal = xremote_cross_remote_item_get_ir_signal(item);
+    dolphin_deed(DolphinDeedIrSend);
+    xremote_scene_ir_notification_message(app, InfraredNotificationMessageBlinkStartSend);
+    if(xremote_scene_ir_signal_is_raw(signal)) {
+        InfraredRawSignal* raw = xremote_ir_signal_get_raw_signal(signal);
+        infrared_worker_set_raw_signal(
+            app->ir_worker, raw->timings, raw->timings_size, raw->frequency, raw->duty_cycle);
+    } else {
+        InfraredMessage* message = xremote_ir_signal_get_message(signal);
+        infrared_worker_set_decoded_signal(app->ir_worker, message);
+    }
+    infrared_worker_tx_set_get_signal_callback(
+        app->ir_worker, infrared_worker_tx_get_signal_steady_callback, app);
+    infrared_worker_tx_start(app->ir_worker);
+    app->transmitting = true;
+    uint32_t time = app->ir_timing;
+    if(item->time > 0) {
+        time = item->time;
+    }
+    furi_thread_flags_wait(0, FuriFlagWaitAny, time);
+    xremote_scene_transmit_stop_ir_signal(app);
+}
+
+void xremote_scene_transmit_send_pause(XRemote* app, CrossRemoteItem* item) {
+    app->transmitting = true;
+    xremote_scene_ir_notification_message(app, PauseNotificationMessageBlinkStartSend);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, item->time * 1000);
+    app->transmitting = false;
+    xremote_scene_ir_notification_message(app, PauseNotificationMessageBlinkStop);
+}
+
+void xremote_scene_transmit_send_subghz(XRemote* app, CrossRemoteItem* item) {
+    app->transmitting = true;
+    xremote_scene_ir_notification_message(app, SubGhzNotificationMessageBlinkStartSend);
+    if(furi_string_utf8_length(item->filename) < 3) {
+        xremote_cross_remote_set_transmitting(app->cross_remote, XRemoteTransmittingStop);
+        app->transmitting = false;
+        return;
+    }
+    subghz_send(app, furi_string_get_cstr(item->filename));
+    //furi_thread_flags_wait(0, FuriFlagWaitAny, 2000);
+}
+
+void xremote_scene_transmit_send_signal(void* context, CrossRemoteItem* item) {
+    furi_assert(context);
+    XRemote* app = context;
+    CrossRemote* remote = app->cross_remote;
+
+    if(app->transmitting) {
+        return;
+    }
+
+    xremote_transmit_model_set_name(
+        app->xremote_transmit, xremote_cross_remote_item_get_name(item));
+    xremote_transmit_model_set_type(app->xremote_transmit, item->type);
+    if(item->type == XRemoteRemoteItemTypeInfrared) {
+        xremote_scene_transmit_send_ir_signal(app, item);
+        xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingStop);
+    } else if(item->type == XRemoteRemoteItemTypePause) {
+        xremote_scene_transmit_send_pause(app, item);
+        xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingStop);
+    } else if(item->type == XRemoteRemoteItemTypeSubGhz) {
+        xremote_scene_transmit_send_subghz(app, item);
+    }
+}
+
+void xremote_scene_transmit_run_remote(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    CrossRemote* remote = app->cross_remote;
+
+    size_t item_count = xremote_cross_remote_get_item_count(remote);
+    for(size_t i = 0; i < item_count;) {
+        if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingIdle) {
+            xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingStart);
+            CrossRemoteItem* item = xremote_cross_remote_get_item(remote, i);
+            xremote_scene_transmit_send_signal(app, item);
+            //furi_thread_flags_wait(0, FuriFlagWaitAny, 1000);
+        } else if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingStopSubghz) {
+            i++;
+            app->state_notifications = SubGhzNotificationStateIDLE;
+            app->transmitting = false;
+            subghz_txrx_stop(app->subghz->txrx);
+            xremote_scene_ir_notification_message(app, SubGhzNotificationMessageBlinkStop);
+            xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingIdle);
+            //furi_thread_flags_wait(0, FuriFlagWaitAny, 1000);
+        } else if(xremote_cross_remote_get_transmitting(remote) == XRemoteTransmittingStop) {
+            i++;
+            xremote_cross_remote_set_transmitting(remote, XRemoteTransmittingIdle);
+        }
+    }
+    xremote_scene_ir_notification_message(app, InfraredNotificationMessageBlinkStop);
+
+    scene_manager_previous_scene(app->scene_manager);
+    //xremote_transmit_model_set_name(app->xremote_transmit, xremote_cross_remote_get_name(remote));
+}
+
+void xremote_scene_transmit_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    xremote_transmit_set_callback(app->xremote_transmit, xremote_transmit_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdTransmit);
+    xremote_scene_transmit_run_remote(app);
+}
+
+bool xremote_scene_transmit_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        FURI_LOG_D(TAG, "Custom Event");
+        switch(event.event) {
+        case XRemoteCustomEventViewTransmitterSendStop:
+            FURI_LOG_D("SUBGHZ", "stop event"); // doesn't trigger
+            break;
+        default:
+            break;
+        }
+    } else if(event.type == SceneManagerEventTypeTick) {
+        FURI_LOG_D(TAG, "Tick Event");
+        if(app->state_notifications == SubGhzNotificationStateTx && app->led == 1) {
+            //blink for subghz
+        }
+    }
+
+    return consumed;
+}
+
+void xremote_scene_transmit_on_exit(void* context) {
+    XRemote* app = context;
+    app->transmitting = false;
+    app->state_notifications = SubGhzNotificationStateIDLE;
+}

+ 38 - 0
cross_remote/scenes/xremote_scene_wip.c

@@ -0,0 +1,38 @@
+#include "../xremote.h"
+
+void xremote_scene_wip_on_enter(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    Popup* popup = app->popup;
+
+    //popup_set_icon(popup, 0, 2, &I_DolphinMafia_115x62);
+    popup_set_header(popup, "SubGhz coming soon", 10, 19, AlignLeft, AlignBottom);
+    popup_set_text(popup, "Check back later", 10, 29, AlignLeft, AlignBottom);
+    popup_set_text(popup, "Press back long", 10, 39, AlignLeft, AlignBottom);
+
+    popup_set_callback(popup, xremote_popup_closed_callback);
+    popup_set_context(popup, context);
+    popup_set_timeout(popup, 100);
+    popup_enable_timeout(popup);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdWip);
+}
+
+bool xremote_scene_wip_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    UNUSED(app);
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == XRemoteCustomEventTypePopupClosed) {
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void xremote_scene_wip_on_exit(void* context) {
+    XRemote* app = context;
+    UNUSED(app);
+}

+ 47 - 0
cross_remote/scenes/xremote_scene_xr_list.c

@@ -0,0 +1,47 @@
+#include "../xremote.h"
+
+void xremote_scene_xr_list_on_enter(void* context) {
+    XRemote* app = context;
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, XREMOTE_APP_EXTENSION, &I_xr_10px);
+    browser_options.base_path = XREMOTE_APP_FOLDER;
+
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, XREMOTE_APP_FOLDER);
+
+    bool success = dialog_file_browser_show(
+        //app->dialogs, app->file_path, app->file_path, &browser_options);
+        app->dialogs,
+        app->file_path,
+        path,
+        &browser_options);
+    furi_string_free(path);
+
+    if(success) {
+        //Load Remote into buffer
+        success = xremote_cross_remote_load(app->cross_remote, app->file_path);
+    }
+
+    if(success) {
+        //Load Remote Button View
+        //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+        scene_manager_next_scene(app->scene_manager, XRemoteSceneTransmit);
+    } else {
+        //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+        scene_manager_previous_scene(app->scene_manager);
+    }
+}
+
+bool xremote_scene_xr_list_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    bool consumed = true;
+
+    return consumed;
+}
+
+void xremote_scene_xr_list_on_exit(void* context) {
+    UNUSED(context);
+}

+ 42 - 0
cross_remote/scenes/xremote_scene_xr_list_edit.c

@@ -0,0 +1,42 @@
+#include "../xremote.h"
+
+void xremote_scene_xr_list_edit_on_enter(void* context) {
+    XRemote* app = context;
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, XREMOTE_APP_EXTENSION, &I_xr_10px);
+    browser_options.base_path = XREMOTE_APP_FOLDER;
+
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, XREMOTE_APP_FOLDER);
+
+    bool success = dialog_file_browser_show(app->dialogs, app->file_path, path, &browser_options);
+    furi_string_free(path);
+
+    if(success) {
+        //Load Remote into buffer
+        success = xremote_cross_remote_load(app->cross_remote, app->file_path);
+    }
+
+    if(success) {
+        //Load Remote Button View
+        //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+        scene_manager_next_scene(app->scene_manager, XRemoteSceneXrListEditItem);
+    } else {
+        //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+        scene_manager_previous_scene(app->scene_manager);
+    }
+}
+
+bool xremote_scene_xr_list_edit_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    bool consumed = true;
+
+    return consumed;
+}
+
+void xremote_scene_xr_list_edit_on_exit(void* context) {
+    UNUSED(context);
+}

+ 65 - 0
cross_remote/scenes/xremote_scene_xr_list_edit_item.c

@@ -0,0 +1,65 @@
+#include "../xremote.h"
+
+enum SubmenuIndexEdit {
+    SubmenuIndexRename = 10,
+    SubmenuIndexEdit,
+    SubmenuIndexDelete,
+};
+
+void xremote_scene_xr_list_edit_item_submenu_callback(void* context, uint32_t index) {
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void xremote_scene_xr_list_edit_item_on_enter(void* context) {
+    XRemote* app = context;
+    //submenu_add_item(app->editmenu, "Rename", SubmenuIndexRename, xremote_scene_xr_list_edit_item_submenu_callback, app);
+    submenu_add_item(
+        app->editmenu,
+        "Edit",
+        SubmenuIndexEdit,
+        xremote_scene_xr_list_edit_item_submenu_callback,
+        app);
+    submenu_add_item(
+        app->editmenu,
+        "Delete",
+        SubmenuIndexDelete,
+        xremote_scene_xr_list_edit_item_submenu_callback,
+        app);
+
+    submenu_set_selected_item(
+        app->editmenu, scene_manager_get_scene_state(app->scene_manager, XRemoteSceneMenu));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdEditItem);
+}
+
+bool xremote_scene_xr_list_edit_item_on_event(void* context, SceneManagerEvent event) {
+    XRemote* app = context;
+    if(event.type == SceneManagerEventTypeBack) {
+        //exit app
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexDelete) {
+            xremote_cross_remote_delete(app->cross_remote);
+        } else if(event.event == SubmenuIndexEdit) {
+            strncpy(
+                app->text_store[0],
+                xremote_cross_remote_get_name(app->cross_remote),
+                XREMOTE_MAX_REMOTE_NAME_LENGTH);
+            scene_manager_next_scene(app->scene_manager, XRemoteSceneCreate);
+            return 0;
+        } else if(event.event == SubmenuIndexRename) {
+            //scene_manager_next_scene(app->scene_manager, XRemoteSceneSaveRemoteItem);
+            //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip);
+            return 0;
+        }
+        scene_manager_previous_scene(app->scene_manager);
+    }
+    return 0;
+}
+
+void xremote_scene_xr_list_edit_item_on_exit(void* context) {
+    XRemote* app = context;
+    submenu_reset(app->editmenu);
+}

BIN
cross_remote/screenshots/xremote_1.png


BIN
cross_remote/screenshots/xremote_2.png


BIN
cross_remote/screenshots/xremote_3.png


BIN
cross_remote/screenshots/xremote_4.png


BIN
cross_remote/screenshots/xremote_5.png


BIN
cross_remote/screenshots/xremote_6.png


+ 112 - 0
cross_remote/views/xremote_infoscreen.c

@@ -0,0 +1,112 @@
+#include "xremote_infoscreen.h"
+#include "../xremote.h"
+
+struct XRemoteInfoscreen {
+    View* view;
+    XRemoteInfoscreenCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int some_value;
+} XRemoteInfoscreenModel;
+
+void xremote_infoscreen_set_callback(
+    XRemoteInfoscreen* instance,
+    XRemoteInfoscreenCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void xremote_infoscreen_draw(Canvas* canvas, XRemoteInfoscreenModel* model) {
+    UNUSED(model);
+    char buffer[64];
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "Cross Remote");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignTop, "Chain IR and SubGhz");
+    canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, "Commands");
+
+    snprintf(buffer, sizeof(buffer), "Version: %s", XREMOTE_VERSION);
+    canvas_draw_str_aligned(canvas, 64, 42, AlignCenter, AlignTop, buffer);
+    elements_button_center(canvas, "Back");
+}
+
+static void xremote_infoscreen_model_init(XRemoteInfoscreenModel* const model) {
+    model->some_value = 1;
+}
+
+bool xremote_infoscreen_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    XRemoteInfoscreen* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                XRemoteInfoscreenModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(XRemoteCustomEventInfoscreenOk, instance->context);
+                },
+                true);
+            break;
+        default:
+            break;
+        }
+    }
+    return true;
+}
+
+void xremote_infoscreen_exit(void* context) {
+    furi_assert(context);
+}
+
+void xremote_infoscreen_enter(void* context) {
+    furi_assert(context);
+    XRemoteInfoscreen* instance = (XRemoteInfoscreen*)context;
+    with_view_model(
+        instance->view,
+        XRemoteInfoscreenModel * model,
+        { xremote_infoscreen_model_init(model); },
+        true);
+}
+
+XRemoteInfoscreen* xremote_infoscreen_alloc() {
+    XRemoteInfoscreen* instance = malloc(sizeof(XRemoteInfoscreen));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(XRemoteInfoscreenModel));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)xremote_infoscreen_draw);
+    view_set_input_callback(instance->view, xremote_infoscreen_input);
+    //view_set_enter_callback(instance->view, xremote_infoscreen_enter);
+    //view_set_exit_callback(instance->view, xremote_infoscreen_exit);
+
+    with_view_model(
+        instance->view,
+        XRemoteInfoscreenModel * model,
+        { xremote_infoscreen_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void xremote_infoscreen_free(XRemoteInfoscreen* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, XRemoteInfoscreenModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* xremote_infoscreen_get_view(XRemoteInfoscreen* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 20 - 0
cross_remote/views/xremote_infoscreen.h

@@ -0,0 +1,20 @@
+#pragma once
+
+//#include "../xremote.h"
+#include <gui/view.h>
+#include "../helpers/xremote_custom_event.h"
+
+typedef struct XRemoteInfoscreen XRemoteInfoscreen;
+
+typedef void (*XRemoteInfoscreenCallback)(XRemoteCustomEvent event, void* context);
+
+void xremote_infoscreen_set_callback(
+    XRemoteInfoscreen* xremote_infoscreen,
+    XRemoteInfoscreenCallback callback,
+    void* context);
+
+View* xremote_infoscreen_get_view(XRemoteInfoscreen* instance);
+
+XRemoteInfoscreen* xremote_infoscreen_alloc();
+
+void xremote_infoscreen_free(XRemoteInfoscreen* instance);

+ 138 - 0
cross_remote/views/xremote_pause_set.c

@@ -0,0 +1,138 @@
+#include "xremote_pause_set.h"
+
+struct XRemotePauseSet {
+    View* view;
+    XRemotePauseSetCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int type;
+    const char* name;
+    int time;
+} XRemotePauseSetModel;
+
+static void xremote_pause_set_model_init(XRemotePauseSetModel* const model) {
+    model->type = XRemoteRemoteItemTypePause;
+    model->time = 1;
+}
+
+void xremote_pause_set_set_callback(
+    XRemotePauseSet* instance,
+    XRemotePauseSetCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void xremote_pause_set_draw(Canvas* canvas, XRemotePauseSetModel* model) {
+    char seconds[14];
+    snprintf(seconds, SECONDS_LENGHT, SECONDS_FORMAT, model->time);
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "Pause duration");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_icon(canvas, 59, 22, &I_ButtonUp_10x5);
+    canvas_draw_icon(canvas, 59, 42, &I_ButtonDown_10x5);
+    canvas_draw_str_aligned(canvas, 64, 31, AlignCenter, AlignTop, seconds);
+    elements_button_center(canvas, "Add");
+}
+
+bool xremote_pause_set_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    XRemotePauseSet* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            instance->callback(XRemoteCustomEventPauseSetBack, instance->context);
+            break;
+        case InputKeyLeft:
+        case InputKeyRight:
+            break;
+        case InputKeyUp:
+            with_view_model(
+                instance->view,
+                XRemotePauseSetModel * model,
+                {
+                    model->time++;
+                    if(model->time > 9) {
+                        model->time = 0;
+                    }
+                },
+                true);
+            break;
+        case InputKeyDown:
+            with_view_model(
+                instance->view,
+                XRemotePauseSetModel * model,
+                {
+                    model->time--;
+                    if(model->time < 0) {
+                        model->time = 9;
+                    }
+                },
+                true);
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                XRemotePauseSetModel * model,
+                {
+                    XRemote* app = instance->context;
+                    xremote_cross_remote_add_pause(app->cross_remote, model->time);
+                },
+                true);
+
+            instance->callback(XRemoteCustomEventPauseSetOk, instance->context);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+    return true;
+}
+
+XRemotePauseSet* xremote_pause_set_alloc() {
+    XRemotePauseSet* instance = malloc(sizeof(XRemotePauseSet));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(XRemotePauseSetModel));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)xremote_pause_set_draw);
+    view_set_enter_callback(instance->view, xremote_pause_set_enter);
+    view_set_input_callback(instance->view, xremote_pause_set_input);
+
+    with_view_model(
+        instance->view,
+        XRemotePauseSetModel * model,
+        { xremote_pause_set_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void xremote_pause_set_enter(void* context) {
+    furi_assert(context);
+    XRemotePauseSet* instance = (XRemotePauseSet*)context;
+    with_view_model(
+        instance->view,
+        XRemotePauseSetModel * model,
+        { xremote_pause_set_model_init(model); },
+        true);
+}
+
+View* xremote_pause_set_get_view(XRemotePauseSet* instance) {
+    furi_assert(instance);
+    return instance->view;
+}
+
+void xremote_pause_set_free(XRemotePauseSet* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, XRemotePauseSetModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}

+ 24 - 0
cross_remote/views/xremote_pause_set.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "../xremote.h"
+//#include <gui/view.h>
+#include "../helpers/xremote_custom_event.h"
+
+#define SECONDS_LENGHT 3
+#define SECONDS_FORMAT "%ds"
+
+typedef struct XRemotePauseSet XRemotePauseSet;
+
+typedef void (*XRemotePauseSetCallback)(XRemoteCustomEvent event, void* context);
+
+void xremote_pause_set_set_callback(
+    XRemotePauseSet* instance,
+    XRemotePauseSetCallback callback,
+    void* context);
+
+XRemotePauseSet* xremote_pause_set_alloc();
+void xremote_pause_set_free(XRemotePauseSet* instance);
+
+void xremote_pause_set_enter(void* context);
+
+View* xremote_pause_set_get_view(XRemotePauseSet* instance);

+ 142 - 0
cross_remote/views/xremote_transmit.c

@@ -0,0 +1,142 @@
+#include "xremote_transmit.h"
+
+struct XRemoteTransmit {
+    View* view;
+    XRemoteTransmitCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int type;
+    const char* name;
+    int time;
+} XRemoteTransmitModel;
+
+static void xremote_transmit_model_init(XRemoteTransmitModel* const model) {
+    model->type = XRemoteRemoteItemTypeInfrared;
+    model->time = 1;
+}
+
+void xremote_transmit_model_set_name(XRemoteTransmit* instance, const char* name) {
+    furi_assert(instance);
+    XRemoteTransmitModel* model = view_get_model(instance->view);
+    model->name = name;
+    view_commit_model(instance->view, false);
+}
+
+void xremote_transmit_model_set_type(XRemoteTransmit* instance, int type) {
+    furi_assert(instance);
+    XRemoteTransmitModel* model = view_get_model(instance->view);
+    model->time = 1;
+    model->type = type;
+    view_commit_model(instance->view, false);
+}
+
+void xremote_transmit_set_callback(
+    XRemoteTransmit* instance,
+    XRemoteTransmitCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void xremote_transmit_draw_ir(Canvas* canvas, XRemoteTransmitModel* model) {
+    model->time++;
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_icon(canvas, 0, 0, &I_ir_transmit_128x64);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 74, 5, AlignLeft, AlignTop, "Sending");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 74, 20, AlignLeft, AlignTop, "Infrared");
+    canvas_draw_str_aligned(canvas, 74, 30, AlignLeft, AlignTop, model->name);
+
+    char temp_str[18];
+    snprintf(temp_str, 18, "%u", model->time);
+    canvas_draw_str_aligned(canvas, 74, 40, AlignLeft, AlignTop, temp_str);
+}
+
+void xremote_transmit_draw_pause(Canvas* canvas, XRemoteTransmitModel* model) {
+    model->time++;
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_icon(canvas, 0, 0, &I_pause_128x64);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 74, 5, AlignLeft, AlignTop, "Waiting");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 74, 20, AlignLeft, AlignTop, "Sequence");
+    canvas_draw_str_aligned(canvas, 74, 30, AlignLeft, AlignTop, model->name);
+
+    char temp_str[18];
+    snprintf(temp_str, 18, "%u", model->time);
+    canvas_draw_str_aligned(canvas, 74, 40, AlignLeft, AlignTop, temp_str);
+}
+
+void xremote_transmit_draw_subghz(Canvas* canvas, XRemoteTransmitModel* model) {
+    model->time++;
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_icon(canvas, 0, 0, &I_sg_transmit_128x64);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 74, 5, AlignLeft, AlignTop, "Sending");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 74, 20, AlignLeft, AlignTop, "SubGhz");
+    canvas_draw_str_aligned(canvas, 74, 30, AlignLeft, AlignTop, model->name);
+
+    char temp_str[18];
+    snprintf(temp_str, 18, "%u", model->time);
+    canvas_draw_str_aligned(canvas, 74, 40, AlignLeft, AlignTop, temp_str);
+}
+
+void xremote_transmit_draw(Canvas* canvas, XRemoteTransmitModel* model) {
+    if(model->type == XRemoteRemoteItemTypeInfrared) {
+        xremote_transmit_draw_ir(canvas, model);
+    } else if(model->type == XRemoteRemoteItemTypeSubGhz) {
+        xremote_transmit_draw_subghz(canvas, model);
+    } else if(model->type == XRemoteRemoteItemTypePause) {
+        xremote_transmit_draw_pause(canvas, model);
+    }
+}
+
+XRemoteTransmit* xremote_transmit_alloc() {
+    XRemoteTransmit* instance = malloc(sizeof(XRemoteTransmit));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(XRemoteTransmitModel));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)xremote_transmit_draw);
+    view_set_enter_callback(instance->view, xremote_transmit_enter);
+
+    with_view_model(
+        instance->view,
+        XRemoteTransmitModel * model,
+        { xremote_transmit_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void xremote_transmit_enter(void* context) {
+    furi_assert(context);
+    XRemoteTransmit* instance = (XRemoteTransmit*)context;
+    with_view_model(
+        instance->view,
+        XRemoteTransmitModel * model,
+        { xremote_transmit_model_init(model); },
+        true);
+}
+
+void xremote_transmit_free(XRemoteTransmit* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, XRemoteTransmitModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* xremote_transmit_get_view(XRemoteTransmit* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 24 - 0
cross_remote/views/xremote_transmit.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "../xremote.h"
+#include "../helpers/xremote_custom_event.h"
+
+typedef struct XRemoteTransmit XRemoteTransmit;
+
+typedef void (*XRemoteTransmitCallback)(XRemoteCustomEvent event, void* context);
+
+void xremote_transmit_model_set_name(XRemoteTransmit* instance, const char* name);
+void xremote_transmit_model_set_type(XRemoteTransmit* instance, int type);
+
+void xremote_transmit_set_callback(
+    XRemoteTransmit* instance,
+    XRemoteTransmitCallback callback,
+    void* context);
+
+XRemoteTransmit* xremote_transmit_alloc();
+
+void xremote_transmit_free(XRemoteTransmit* instance);
+
+View* xremote_transmit_get_view(XRemoteTransmit* instance);
+
+void xremote_transmit_enter(void* context);

+ 214 - 0
cross_remote/xremote.c

@@ -0,0 +1,214 @@
+#include "xremote.h"
+
+bool xremote_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    XRemote* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+void xremote_tick_event_callback(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+//leave app if back button pressed
+bool xremote_navigation_event_callback(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+XRemote* xremote_app_alloc() {
+    XRemote* app = malloc(sizeof(XRemote));
+    app->gui = furi_record_open(RECORD_GUI);
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+
+    //Turn backlight on, believe me this makes testing your app easier
+    notification_message(app->notification, &sequence_display_backlight_on);
+
+    //Scene additions
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&xremote_scene_handlers, app);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, xremote_navigation_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, xremote_tick_event_callback, 100);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, xremote_custom_event_callback);
+    app->submenu = submenu_alloc();
+    app->editmenu = submenu_alloc();
+
+    // Set defaults, in case no config loaded
+    app->haptic = 1;
+    app->speaker = 1;
+    app->led = 1;
+    app->save_settings = 1;
+    app->transmitting = 0;
+    app->ir_timing = 1000;
+    app->ir_timing_char = "1000";
+    app->sg_timing = 500;
+    app->sg_timing_char = "500";
+    app->stop_transmit = false;
+
+    // Load configs
+    xremote_read_settings(app);
+
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+    app->file_path = furi_string_alloc();
+
+    app->ir_remote_buffer = xremote_ir_remote_alloc();
+    app->ir_worker = infrared_worker_alloc();
+    app->cross_remote = xremote_cross_remote_alloc();
+
+    app->sg_remote_buffer = xremote_sg_remote_alloc();
+
+    app->loading = loading_alloc();
+
+    app->subghz = subghz_alloc();
+
+    app->text_input = text_input_alloc();
+
+    // Custom made int keyboard
+    app->int_input = int_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdIntInput, int_input_get_view(app->int_input));
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdTextInput, text_input_get_view(app->text_input));
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdMenu, submenu_get_view(app->submenu));
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdEditItem, submenu_get_view(app->editmenu));
+    app->xremote_infoscreen = xremote_infoscreen_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        XRemoteViewIdInfoscreen,
+        xremote_infoscreen_get_view(app->xremote_infoscreen));
+    app->xremote_transmit = xremote_transmit_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        XRemoteViewIdTransmit,
+        xremote_transmit_get_view(app->xremote_transmit));
+    app->xremote_pause_set = xremote_pause_set_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        XRemoteViewIdPauseSet,
+        xremote_pause_set_get_view(app->xremote_pause_set));
+    app->button_menu_create = button_menu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdCreate, button_menu_get_view(app->button_menu_create));
+    app->button_menu_create_add = button_menu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        XRemoteViewIdCreateAdd,
+        button_menu_get_view(app->button_menu_create_add));
+    app->button_menu_ir = button_menu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdIrRemote, button_menu_get_view(app->button_menu_ir));
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        XRemoteViewIdSettings,
+        variable_item_list_get_view(app->variable_item_list));
+
+    app->popup = popup_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, XRemoteViewIdWip, popup_get_view(app->popup));
+    app->view_stack = view_stack_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, XRemoteViewIdStack, view_stack_get_view(app->view_stack));
+
+    //End Scene Additions
+
+    return app;
+}
+
+void xremote_app_free(XRemote* app) {
+    furi_assert(app);
+
+    // Scene manager
+    scene_manager_free(app->scene_manager);
+
+    infrared_worker_free(app->ir_worker);
+
+    xremote_cross_remote_free(app->cross_remote);
+
+    subghz_free(app->subghz);
+
+    // View Dispatcher
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdEditItem);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdInfoscreen);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdCreate);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdCreateAdd);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdSettings);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdWip);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdStack);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdTextInput);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdIntInput);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdTransmit);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdPauseSet);
+    view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdIrRemote);
+    text_input_free(app->text_input);
+    int_input_free(app->int_input);
+    button_menu_free(app->button_menu_create);
+    button_menu_free(app->button_menu_create_add);
+    button_menu_free(app->button_menu_ir);
+    view_stack_free(app->view_stack);
+    popup_free(app->popup);
+    submenu_free(app->submenu);
+    xremote_infoscreen_free(app->xremote_infoscreen);
+    xremote_pause_set_free(app->xremote_pause_set);
+    xremote_transmit_free(app->xremote_transmit);
+
+    view_dispatcher_free(app->view_dispatcher);
+    furi_record_close(RECORD_GUI);
+
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(app->file_path);
+
+    loading_free(app->loading);
+
+    app->gui = NULL;
+    app->notification = NULL;
+
+    //Remove whatever is left
+    free(app);
+}
+
+void xremote_popup_closed_callback(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, XRemoteCustomEventTypePopupClosed);
+}
+
+void xremote_text_input_callback(void* context) {
+    furi_assert(context);
+    XRemote* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, XRemoteCustomEventTextInput);
+}
+
+int32_t xremote_app(void* p) {
+    UNUSED(p);
+    XRemote* app = xremote_app_alloc();
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    //scene_manager_next_scene(app->scene_manager, XRemoteSceneInfoscreen); //Start with start screen
+    scene_manager_next_scene(
+        app->scene_manager, XRemoteSceneMenu); //if you want to directly start with Menu
+
+    furi_hal_power_suppress_charge_enter();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    xremote_save_settings(app);
+
+    furi_hal_power_suppress_charge_exit();
+    xremote_app_free(app);
+
+    return 0;
+}

+ 95 - 0
cross_remote/xremote.h

@@ -0,0 +1,95 @@
+#pragma once
+
+#include "scenes/xremote_scene.h"
+#include "views/xremote_infoscreen.h"
+#include "views/xremote_transmit.h"
+#include "views/xremote_pause_set.h"
+#include "helpers/xremote_storage.h"
+#include "models/infrared/xremote_ir_remote.h"
+#include "models/cross/xremote_cross_remote.h"
+#include "helpers/subghz/subghz_types.h"
+#include "helpers/subghz/subghz.h"
+#include "helpers/gui/int_input.h"
+#include "xremote_i.h"
+
+typedef struct SubGhz SubGhz;
+
+typedef struct {
+    Gui* gui;
+    DialogsApp* dialogs;
+    FuriString* file_path;
+    NotificationApp* notification;
+    SubGhzNotificationState state_notifications;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    Submenu* editmenu;
+    ButtonMenu* button_menu_create;
+    ButtonMenu* button_menu_create_add;
+    ButtonMenu* button_menu_ir;
+    TextInput* text_input;
+    Popup* popup;
+    Loading* loading;
+    ViewStack* view_stack;
+    SceneManager* scene_manager;
+    VariableItemList* variable_item_list;
+    XRemoteInfoscreen* xremote_infoscreen;
+    XRemoteTransmit* xremote_transmit;
+    XRemotePauseSet* xremote_pause_set;
+    InfraredRemote* ir_remote_buffer;
+    InfraredWorker* ir_worker;
+    SubGhzRemote* sg_remote_buffer;
+    CrossRemote* cross_remote;
+    uint32_t haptic;
+    uint32_t speaker;
+    uint32_t led;
+    uint32_t save_settings;
+    uint32_t edit_item;
+    uint32_t ir_timing;
+    char* ir_timing_char;
+    uint32_t sg_timing;
+    char* sg_timing_char;
+    bool transmitting;
+    bool stop_transmit;
+    char text_store[XREMOTE_TEXT_STORE_NUM][XREMOTE_TEXT_STORE_SIZE + 1];
+    SubGhz* subghz;
+    IntInput* int_input;
+} XRemote;
+
+typedef enum {
+    XRemoteViewIdInfoscreen,
+    XRemoteViewIdMenu,
+    XRemoteViewIdEditItem,
+    XRemoteViewIdCreate,
+    XRemoteViewIdCreateAdd,
+    XRemoteViewIdSettings,
+    XRemoteViewIdWip,
+    XRemoteViewIdIrRemote,
+    XRemoteViewIdStack,
+    XRemoteViewIdTextInput,
+    XRemoteViewIdIntInput,
+    XRemoteViewIdTransmit,
+    XRemoteViewIdPauseSet,
+} XRemoteViewId;
+
+typedef enum {
+    XRemoteHapticOff,
+    XRemoteHapticOn,
+} XRemoteHapticState;
+
+typedef enum {
+    XRemoteSpeakerOff,
+    XRemoteSpeakerOn,
+} XRemoteSpeakerState;
+
+typedef enum {
+    XRemoteLedOff,
+    XRemoteLedOn,
+} XRemoteLedState;
+
+typedef enum {
+    XRemoteSettingsOff,
+    XRemoteSettingsOn,
+} XRemoteSettingsStoreState;
+
+void xremote_popup_closed_callback(void* context);
+void xremote_text_input_callback(void* context);

+ 115 - 0
cross_remote/xremote_i.h

@@ -0,0 +1,115 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <gui/view.h>
+#include <gui/view_stack.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/button_panel.h>
+#include <gui/modules/variable_item_list.h>
+
+//#include <lib/subghz/protocols/protocol_items.h> //Not found
+
+#include <input/input.h>
+#include <notification/notification_messages.h>
+
+#include <string.h>
+#include <m-array.h>
+#include <toolbox/path.h>
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <core/check.h>
+#include <core/common_defines.h>
+#include <dolphin/dolphin.h>
+#include <infrared.h>
+#include <infrared_worker.h>
+#include <infrared_transmit.h>
+#include <flipper_format/flipper_format.h>
+#include "xremote_icons.h"
+#include "models/subghz/xremote_sg_remote.h"
+
+#define XREMOTE_APP_FOLDER EXT_PATH("apps_data/xremote")
+#define XREMOTE_DEFAULT_REMOTE_NAME "remote"
+#define XREMOTE_APP_EXTENSION ".xr"
+#define XREMOTE_FILE_TYPE "Cross Remote File"
+#define XREMOTE_FILE_VERSION 1
+#define XREMOTE_TEXT_STORE_NUM 3
+#define XREMOTE_TEXT_STORE_SIZE 128
+#define XREMOTE_MAX_ITEM_NAME_LENGTH 22
+#define XREMOTE_MAX_REMOTE_NAME_LENGTH 22
+#define XREMOTE_VERSION "2.5"
+
+#define INFRARED_APP_EXTENSION ".ir"
+#define INFRARED_APP_FOLDER ANY_PATH("infrared")
+
+#define SUBGHZ_APP_EXTENSION ".sub"
+#define SUBGHZ_APP_FOLDER ANY_PATH("subghz")
+
+#define TAG "XRemote"
+
+typedef enum {
+    XRemoteRemoteItemTypeInfrared,
+    XRemoteRemoteItemTypeSubGhz,
+    XRemoteRemoteItemTypePause,
+} XRemoteRemoteItemType;
+
+typedef enum {
+    InfraredNotificationMessageSuccess,
+    InfraredNotificationMessageGreenOn,
+    InfraredNotificationMessageGreenOff,
+    InfraredNotificationMessageYellowOn,
+    InfraredNotificationMessageYellowOff,
+    InfraredNotificationMessageBlinkStartRead,
+    InfraredNotificationMessageBlinkStartSend,
+    InfraredNotificationMessageBlinkStop,
+    PauseNotificationMessageBlinkStartSend,
+    PauseNotificationMessageBlinkStop,
+    SubGhzNotificationMessageBlinkStartSend,
+    SubGhzNotificationMessageBlinkStop,
+} XRemoteNotificationMessage;
+
+typedef enum {
+    XRemoteTransmittingIdle,
+    XRemoteTransmittingStart,
+    XRemoteTransmittingStop,
+    XRemoteTransmittingStopSubghz,
+} XRemoteRemoteTransmissionStatus;
+
+struct InfraredSignal {
+    bool is_raw;
+    union {
+        InfraredMessage message;
+        InfraredRawSignal raw;
+    } payload;
+};
+
+struct CrossRemoteItem {
+    FuriString* name;
+    FuriString* filename;
+    InfraredSignal* ir_signal;
+    SubGhzRemote* sg_signal;
+    int16_t type;
+    uint32_t time;
+};
+
+typedef struct CrossRemote CrossRemote;
+typedef struct CrossRemoteItem CrossRemoteItem;
+
+typedef struct XRemoteTransmit XRemoteTransmit;
+typedef struct XRemotePauseSet XRemotePauseSet;