Przeglądaj źródła

Add subghz_remote from https://github.com/DarkFlippers/SubGHz_Remote

git-subtree-dir: subghz_remote
git-subtree-mainline: bae8713488abf40f92ea0879410f248dea041218
git-subtree-split: 73ca3f2ac02e313004aa3a724f2f8a0e4646ad10
Willy-JL 1 rok temu
rodzic
commit
87b3ffb966
62 zmienionych plików z 3840 dodań i 0 usunięć
  1. 4 0
      subghz_remote/.gitignore
  2. 1 0
      subghz_remote/.gitsubtree
  3. 21 0
      subghz_remote/LICENSE
  4. 43 0
      subghz_remote/application.fam
  5. 25 0
      subghz_remote/catalog/docs/Changelog.md
  6. 24 0
      subghz_remote/catalog/docs/Readme.md
  7. BIN
      subghz_remote/catalog/screenshots/Editor_main.png
  8. BIN
      subghz_remote/catalog/screenshots/Editor_submenu.png
  9. BIN
      subghz_remote/catalog/screenshots/Remote_idle.png
  10. BIN
      subghz_remote/catalog/screenshots/Remote_send.png
  11. 58 0
      subghz_remote/helpers/subrem_custom_event.h
  12. 192 0
      subghz_remote/helpers/subrem_presets.c
  13. 39 0
      subghz_remote/helpers/subrem_presets.h
  14. 48 0
      subghz_remote/helpers/subrem_types.h
  15. 4 0
      subghz_remote/helpers/txrx/Readme.md
  16. 672 0
      subghz_remote/helpers/txrx/subghz_txrx.c
  17. 375 0
      subghz_remote/helpers/txrx/subghz_txrx.h
  18. 31 0
      subghz_remote/helpers/txrx/subghz_txrx_i.h
  19. BIN
      subghz_remote/icon.png
  20. BIN
      subghz_remote/icons/DolphinNice_96x59.png
  21. BIN
      subghz_remote/icons/remote_scene/Dpad/down.png
  22. BIN
      subghz_remote/icons/remote_scene/Dpad/down_hover.png
  23. BIN
      subghz_remote/icons/remote_scene/Dpad/left.png
  24. BIN
      subghz_remote/icons/remote_scene/Dpad/left_hover.png
  25. BIN
      subghz_remote/icons/remote_scene/Dpad/ok.png
  26. BIN
      subghz_remote/icons/remote_scene/Dpad/ok_hover.png
  27. BIN
      subghz_remote/icons/remote_scene/Dpad/right.png
  28. BIN
      subghz_remote/icons/remote_scene/Dpad/right_hover.png
  29. BIN
      subghz_remote/icons/remote_scene/Dpad/up.png
  30. BIN
      subghz_remote/icons/remote_scene/Dpad/up_hover.png
  31. BIN
      subghz_remote/icons/remote_scene/list/ButtonDown_7x4.png
  32. BIN
      subghz_remote/icons/remote_scene/list/ButtonLeft_4x7.png
  33. BIN
      subghz_remote/icons/remote_scene/list/ButtonRight_4x7.png
  34. BIN
      subghz_remote/icons/remote_scene/list/ButtonUp_7x4.png
  35. BIN
      subghz_remote/icons/remote_scene/list/Ok_btn_9x9.png
  36. BIN
      subghz_remote/icons/remote_scene/statusbar/External_antenna_20x12.png
  37. BIN
      subghz_remote/icons/remote_scene/statusbar/Internal_antenna_20x12.png
  38. BIN
      subghz_remote/icons/remote_scene/statusbar/Pin_arrow_up_7x9.png
  39. BIN
      subghz_remote/icons/remote_scene/statusbar/Status_cube_14x14.png
  40. BIN
      subghz_remote/icons/remote_scene/statusbar/status_bar.png
  41. BIN
      subghz_remote/icons/sub1_10px.png
  42. BIN
      subghz_remote/icons/subrem_10px.png
  43. 30 0
      subghz_remote/scenes/subrem_scene.c
  44. 29 0
      subghz_remote/scenes/subrem_scene.h
  45. 12 0
      subghz_remote/scenes/subrem_scene_config.h
  46. 133 0
      subghz_remote/scenes/subrem_scene_edit_label.c
  47. 123 0
      subghz_remote/scenes/subrem_scene_edit_menu.c
  48. 74 0
      subghz_remote/scenes/subrem_scene_edit_preview.c
  49. 54 0
      subghz_remote/scenes/subrem_scene_edit_submenu.c
  50. 70 0
      subghz_remote/scenes/subrem_scene_enter_new_name.c
  51. 129 0
      subghz_remote/scenes/subrem_scene_fw_warning.c
  52. 29 0
      subghz_remote/scenes/subrem_scene_open_map_file.c
  53. 119 0
      subghz_remote/scenes/subrem_scene_open_sub_file.c
  54. 118 0
      subghz_remote/scenes/subrem_scene_remote.c
  55. 100 0
      subghz_remote/scenes/subrem_scene_start.c
  56. 218 0
      subghz_remote/subghz_remote_app.c
  57. 317 0
      subghz_remote/subghz_remote_app_i.c
  58. 68 0
      subghz_remote/subghz_remote_app_i.h
  59. 290 0
      subghz_remote/views/edit_menu.c
  60. 29 0
      subghz_remote/views/edit_menu.h
  61. 323 0
      subghz_remote/views/remote.c
  62. 38 0
      subghz_remote/views/remote.h

+ 4 - 0
subghz_remote/.gitignore

@@ -0,0 +1,4 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig

+ 1 - 0
subghz_remote/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/DarkFlippers/SubGHz_Remote ufw_main_app /

+ 21 - 0
subghz_remote/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 DarkFlippers @gid9798 @xMasterX
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 43 - 0
subghz_remote/application.fam

@@ -0,0 +1,43 @@
+# App(
+#     appid="subghz_remote_ofw",
+#     name="Sub-GHz Remote",
+#     apptype=FlipperAppType.EXTERNAL,
+#     entry_point="subghz_remote_app",
+#     requires=[
+#         "gui",
+#         "dialogs",
+#     ],
+#     stack_size=2 * 1024,
+#     targets=["f7"],
+#     fap_icon="icon.png",
+#     fap_author="gid9798 xMasterX",
+#     fap_description="SubGhz Remote, uses up to 5 .sub files",
+#     fap_category="Sub-GHz",
+#     fap_icon_assets="icons",
+#     fap_icon_assets_symbol="subghz_remote",
+#     fap_version="1.2",
+#     fap_weburl="https://github.com/DarkFlippers/SubGHz_Remote",
+# )
+
+App(
+    appid="subghz_remote",
+    name="Sub-GHz Remote",
+    apptype=FlipperAppType.MENUEXTERNAL,
+    entry_point="subghz_remote_app",
+    icon="A_SubGHzRemote_14",
+    order=11,
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    stack_size=2 * 1024,
+    targets=["f7"],
+    fap_icon="icon.png",
+    fap_author="gid9798 xMasterX",
+    fap_description="SubGhz Remote, uses up to 5 .sub files",
+    fap_category="Sub-GHz",
+    fap_icon_assets="icons",
+    fap_icon_assets_symbol="subghz_remote",
+    fap_version="1.2",
+    fap_weburl="https://github.com/DarkFlippers/SubGHz_Remote",
+)

+ 25 - 0
subghz_remote/catalog/docs/Changelog.md

@@ -0,0 +1,25 @@
+## v1.2
+- **Official FirmWare Support**
+- Add warning screen on CustomFW
+    - The .sub file format may differ from the official one and may be broken
+
+## v1.1
+- **Was combined with a configuration plugin**
+    - Editing/Creating map file
+- Support for starting arguments
+
+## v1.0
+
+**Initial implementation:**
+- Transmission
+- GUI
+- All .sub files for which transfer is available are supported
+- Signal types:
+    - Static
+    - Dynamic
+    - RAW
+    - BinRAW
+
+*Custom modulations are not supported yet*
+
+**Map File Format** - FlipperFormat .txt file

+ 24 - 0
subghz_remote/catalog/docs/Readme.md

@@ -0,0 +1,24 @@
+With this application, you can combine up to 5 .sub files into one remote, and use flipper as a remote with multiple buttons.
+## What is "Map" Files?
+"Map" is short for mapping
+A Map Files is a .txt files that the application uses to store information about remotes
+# How to use
+## First screen 
+After launching the application, you will see the MAP file selection screen (file browser).
+- Select map file or press "back" to go Main menu
+## Main menu
+- Open map file - switching to remote
+    - Select map file 
+    - On remote screen, use the navigation buttons(D-pad) to send a signal
+- Edit Map File - map file editor
+    - Select map file 
+    - Up/Down - slot nafigation
+    - Ok - edit menu
+    - Left - preview/save
+- New Map File - Creating a new map file
+    - Enter a name
+    - The rest is similar to map file editor
+# About map file
+Map file - FlipperFormat .txt file.
+
+Stores custom names, and paths to used .sub files.

BIN
subghz_remote/catalog/screenshots/Editor_main.png


BIN
subghz_remote/catalog/screenshots/Editor_submenu.png


BIN
subghz_remote/catalog/screenshots/Remote_idle.png


BIN
subghz_remote/catalog/screenshots/Remote_send.png


+ 58 - 0
subghz_remote/helpers/subrem_custom_event.h

@@ -0,0 +1,58 @@
+#pragma once
+
+typedef enum {
+    SubRemEditMenuStateUP = 0,
+    SubRemEditMenuStateDOWN,
+    SubRemEditMenuStateLEFT,
+    SubRemEditMenuStateRIGHT,
+    SubRemEditMenuStateOK,
+} SubRemEditMenuState;
+
+typedef enum {
+    // StartSubmenuIndex
+    SubmenuIndexSubRemOpenMapFile = 0,
+    SubmenuIndexSubRemEditMapFile,
+    SubmenuIndexSubRemNewMapFile,
+#if FURI_DEBUG
+    SubmenuIndexSubRemRemoteView,
+#endif
+    // SubmenuIndexSubRemAbout,
+
+    // EditSubmenuIndex
+    EditSubmenuIndexEditLabel,
+    EditSubmenuIndexEditFile,
+
+    // SubRemCustomEvent
+    SubRemCustomEventViewRemoteStartUP = 100,
+    SubRemCustomEventViewRemoteStartDOWN,
+    SubRemCustomEventViewRemoteStartLEFT,
+    SubRemCustomEventViewRemoteStartRIGHT,
+    SubRemCustomEventViewRemoteStartOK,
+    SubRemCustomEventViewRemoteBack,
+    SubRemCustomEventViewRemoteStop,
+    SubRemCustomEventViewRemoteForcedStop,
+
+    SubRemCustomEventViewEditMenuBack,
+    SubRemCustomEventViewEditMenuUP,
+    SubRemCustomEventViewEditMenuDOWN,
+    SubRemCustomEventViewEditMenuEdit,
+    SubRemCustomEventViewEditMenuSave,
+
+    SubRemCustomEventSceneEditsubmenu,
+    SubRemCustomEventSceneEditLabelInputDone,
+    SubRemCustomEventSceneEditLabelWidgetAcces,
+    SubRemCustomEventSceneEditLabelWidgetBack,
+
+    SubRemCustomEventSceneEditOpenSubErrorPopup,
+
+    SubRemCustomEventSceneEditPreviewSaved,
+
+    SubRemCustomEventSceneNewName,
+
+#ifdef FW_ORIGIN_Official
+    SubRemCustomEventSceneFwWarningExit,
+    SubRemCustomEventSceneFwWarningNext,
+    SubRemCustomEventSceneFwWarningContinue,
+#endif
+
+} SubRemCustomEvent;

+ 192 - 0
subghz_remote/helpers/subrem_presets.c

@@ -0,0 +1,192 @@
+#include "subrem_presets.h"
+
+#define TAG "SubRemPresets"
+
+SubRemSubFilePreset* subrem_sub_file_preset_alloc(void) {
+    SubRemSubFilePreset* sub_preset = malloc(sizeof(SubRemSubFilePreset));
+
+    sub_preset->fff_data = flipper_format_string_alloc();
+    sub_preset->file_path = furi_string_alloc();
+    sub_preset->protocaol_name = furi_string_alloc();
+    sub_preset->label = furi_string_alloc();
+
+    sub_preset->freq_preset.name = furi_string_alloc();
+
+    sub_preset->type = SubGhzProtocolTypeUnknown;
+    sub_preset->load_state = SubRemLoadSubStateNotSet;
+
+    return sub_preset;
+}
+
+void subrem_sub_file_preset_free(SubRemSubFilePreset* sub_preset) {
+    furi_assert(sub_preset);
+
+    furi_string_free(sub_preset->label);
+    furi_string_free(sub_preset->protocaol_name);
+    furi_string_free(sub_preset->file_path);
+    flipper_format_free(sub_preset->fff_data);
+
+    furi_string_free(sub_preset->freq_preset.name);
+
+    free(sub_preset);
+}
+
+void subrem_sub_file_preset_reset(SubRemSubFilePreset* sub_preset) {
+    furi_assert(sub_preset);
+
+    furi_string_set_str(sub_preset->label, "");
+    furi_string_reset(sub_preset->protocaol_name);
+    furi_string_reset(sub_preset->file_path);
+
+    Stream* fff_data_stream = flipper_format_get_raw_stream(sub_preset->fff_data);
+    stream_clean(fff_data_stream);
+
+    sub_preset->type = SubGhzProtocolTypeUnknown;
+    sub_preset->load_state = SubRemLoadSubStateNotSet;
+}
+
+SubRemLoadSubState subrem_sub_preset_load(
+    SubRemSubFilePreset* sub_preset,
+    SubGhzTxRx* txrx,
+    FlipperFormat* fff_data_file) {
+    furi_assert(sub_preset);
+    furi_assert(txrx);
+    furi_assert(fff_data_file);
+
+    Stream* fff_data_stream = flipper_format_get_raw_stream(sub_preset->fff_data);
+
+    SubRemLoadSubState ret;
+    FuriString* temp_str = furi_string_alloc();
+    uint32_t temp_data32;
+    uint32_t repeat = 200;
+
+    ret = SubRemLoadSubStateError;
+
+    do {
+        stream_clean(fff_data_stream);
+        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;
+        }
+
+        SubGhzSetting* setting = subghz_txrx_get_setting(txrx);
+
+        //Load frequency or using default from settings
+        ret = SubRemLoadSubStateErrorFreq;
+        if(!flipper_format_read_uint32(fff_data_file, "Frequency", &temp_data32, 1)) {
+            FURI_LOG_W(TAG, "Cannot read frequency. Set default frequency");
+            sub_preset->freq_preset.frequency = subghz_setting_get_default_frequency(setting);
+        } else if(!subghz_txrx_radio_device_is_frequency_valid(txrx, temp_data32)) {
+            FURI_LOG_E(TAG, "Frequency not supported on chosen radio module");
+            break;
+        }
+        sub_preset->freq_preset.frequency = temp_data32;
+
+        //Load preset
+        ret = SubRemLoadSubStateErrorMod;
+        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(txrx, furi_string_get_cstr(temp_str)));
+        if(!strcmp(furi_string_get_cstr(temp_str), "")) {
+            break;
+        }
+
+        if(!strcmp(furi_string_get_cstr(temp_str), "CUSTOM")) {
+            //TODO Does this work properly?
+            //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;
+            }
+            // FURI_LOG_E(TAG, "CUSTOM preset is not supported");
+            // break;
+            // TODO Custom preset loading logic if need
+            // sub_preset->freq_preset.preset_index =
+            //     subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(temp_str));
+        }
+
+        furi_string_set(sub_preset->freq_preset.name, temp_str);
+
+        // Load protocol
+        ret = SubRemLoadSubStateErrorProtocol;
+        if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
+            FURI_LOG_E(TAG, "Missing Protocol");
+            break;
+        }
+
+        FlipperFormat* fff_data = sub_preset->fff_data;
+        if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) {
+            //if RAW
+            subghz_protocol_raw_gen_fff_data(
+                fff_data,
+                furi_string_get_cstr(sub_preset->file_path),
+                subghz_txrx_radio_device_get_name(txrx));
+        } else {
+            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(txrx, furi_string_get_cstr(temp_str))) {
+            SubGhzProtocolStatus status =
+                subghz_protocol_decoder_base_deserialize(subghz_txrx_get_decoder(txrx), fff_data);
+            if(status != SubGhzProtocolStatusOk) {
+                break;
+            }
+        } else {
+            FURI_LOG_E(TAG, "Protocol not found");
+            break;
+        }
+
+        const SubGhzProtocol* protocol = subghz_txrx_get_decoder(txrx)->protocol;
+
+        if(protocol->flag & SubGhzProtocolFlag_Send) {
+            if((protocol->type == SubGhzProtocolTypeStatic) ||
+               (protocol->type == SubGhzProtocolTypeDynamic) ||
+#ifndef FW_ORIGIN_Official
+               (protocol->type == SubGhzProtocolTypeBinRAW) ||
+#endif
+               (protocol->type == SubGhzProtocolTypeRAW)) {
+                sub_preset->type = protocol->type;
+            } else {
+                FURI_LOG_E(TAG, "Unsuported Protocol");
+                break;
+            }
+
+            furi_string_set(sub_preset->protocaol_name, temp_str);
+        } else {
+            FURI_LOG_E(TAG, "Protocol does not support transmission");
+            break;
+        }
+
+        if(!flipper_format_insert_or_update_uint32(fff_data, "Repeat", &repeat, 1)) {
+            FURI_LOG_E(TAG, "Unable Repeat");
+            break;
+        }
+
+        ret = SubRemLoadSubStateOK;
+
+#if FURI_DEBUG
+        FURI_LOG_I(TAG, "%-16s - protocol Loaded", furi_string_get_cstr(sub_preset->label));
+#endif
+    } while(false);
+
+    furi_string_free(temp_str);
+    sub_preset->load_state = ret;
+    return ret;
+}

+ 39 - 0
subghz_remote/helpers/subrem_presets.h

@@ -0,0 +1,39 @@
+#pragma once
+
+#include "subrem_types.h"
+#include "txrx/subghz_txrx.h"
+
+#include <flipper_format/flipper_format_i.h>
+#include <lib/subghz/types.h>
+
+typedef struct {
+    FuriString* name;
+    uint32_t frequency;
+    // size_t preset_index; // Need for custom preset
+} FreqPreset;
+
+// Sub File preset
+typedef struct {
+    FlipperFormat* fff_data;
+    FreqPreset freq_preset;
+    FuriString* file_path;
+    FuriString* protocaol_name;
+    FuriString* label;
+    SubGhzProtocolType type;
+    SubRemLoadSubState load_state;
+} SubRemSubFilePreset;
+
+typedef struct {
+    SubRemSubFilePreset* subs_preset[SubRemSubKeyNameMaxCount];
+} SubRemMapPreset;
+
+SubRemSubFilePreset* subrem_sub_file_preset_alloc(void);
+
+void subrem_sub_file_preset_free(SubRemSubFilePreset* sub_preset);
+
+void subrem_sub_file_preset_reset(SubRemSubFilePreset* sub_preset);
+
+SubRemLoadSubState subrem_sub_preset_load(
+    SubRemSubFilePreset* sub_preset,
+    SubGhzTxRx* txrx,
+    FlipperFormat* fff_data_file);

+ 48 - 0
subghz_remote/helpers/subrem_types.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#define SUBREM_APP_APP_FILE_VERSION 1
+#define SUBREM_APP_APP_FILE_TYPE "Flipper SubRem Map file"
+#define SUBREM_APP_EXTENSION ".txt"
+
+typedef enum {
+    SubRemSubKeyNameUp = (0U),
+    SubRemSubKeyNameDown,
+    SubRemSubKeyNameLeft,
+    SubRemSubKeyNameRight,
+    SubRemSubKeyNameOk,
+    SubRemSubKeyNameMaxCount,
+} SubRemSubKeyName;
+
+typedef enum {
+    SubRemViewIDSubmenu,
+    SubRemViewIDWidget,
+    SubRemViewIDPopup,
+    SubRemViewIDTextInput,
+    SubRemViewIDRemote,
+    SubRemViewIDEditMenu,
+} SubRemViewID;
+
+typedef enum {
+    SubRemLoadSubStateNotSet = 0,
+    SubRemLoadSubStatePreloaded,
+    SubRemLoadSubStateError,
+    SubRemLoadSubStateErrorIncorectPath,
+    SubRemLoadSubStateErrorNoFile,
+    SubRemLoadSubStateErrorFreq,
+    SubRemLoadSubStateErrorMod,
+    SubRemLoadSubStateErrorProtocol,
+    SubRemLoadSubStateOK,
+} SubRemLoadSubState;
+
+typedef enum {
+    SubRemLoadMapStateBack = 0,
+    SubRemLoadMapStateError,
+    SubRemLoadMapStateErrorOpenError,
+    SubRemLoadMapStateErrorStorage,
+    SubRemLoadMapStateErrorBrokenFile,
+    SubRemLoadMapStateNotAllOK,
+    SubRemLoadMapStateOK,
+} SubRemLoadMapState;

+ 4 - 0
subghz_remote/helpers/txrx/Readme.md

@@ -0,0 +1,4 @@
+This is part of the official `SubGhz` app from [flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware/tree/3217f286f03da119398586daf94c0723d28b872a/applications/main/subghz)
+
+With changes from [unleashed-firmware
+](https://github.com/DarkFlippers/unleashed-firmware/tree/3eac6ccd48a3851cf5d63bf7899b387a293e5319/applications/main/subghz)

+ 672 - 0
subghz_remote/helpers/txrx/subghz_txrx.c

@@ -0,0 +1,672 @@
+#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>
+
+#ifndef FW_ORIGIN_Official
+#include <lib/subghz/blocks/custom_btn.h>
+#endif
+
+#define TAG "SubGhz"
+
+static void subghz_txrx_radio_device_power_on(SubGhzTxRx* instance) {
+    UNUSED(instance);
+    uint8_t attempts = 0;
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        //CC1101 power-up time
+        furi_delay_ms(10);
+    }
+}
+
+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(void) {
+    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);
+    subghz_txrx_set_debug_pin_state(instance, false);
+
+    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_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 Internal
+    subghz_devices_init();
+    instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
+    instance->radio_device_type =
+        subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101);
+
+    return instance;
+}
+
+void subghz_txrx_free(SubGhzTxRx* instance) {
+    furi_assert(instance);
+
+    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,
+    bool long_name) {
+    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) {
+        if(long_name) {
+            furi_string_printf(modulation, "%s", furi_string_get_cstr(preset->name));
+        } else {
+            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;
+}
+
+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);
+    furi_assert(instance->txrx_state != SubGhzTxRxStateSleep);
+    subghz_devices_idle(instance->radio_device);
+    subghz_txrx_speaker_off(instance);
+    instance->txrx_state = SubGhzTxRxStateIDLE;
+}
+
+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;
+    }
+
+    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;
+    do {
+        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;
+        }
+        ret = SubGhzTxRxStartTxStateOk;
+
+        SubGhzRadioPreset* preset = instance->preset;
+        instance->transmitter =
+            subghz_transmitter_alloc_init(instance->environment, furi_string_get_cstr(temp_str));
+
+        if(instance->transmitter) {
+            if(subghz_transmitter_deserialize(instance->transmitter, flipper_format) ==
+               SubGhzProtocolStatusOk) {
+                if(strcmp(furi_string_get_cstr(preset->name), "") != 0) {
+                    subghz_txrx_begin(
+                        instance,
+                        subghz_setting_get_preset_data_by_name(
+                            instance->setting, furi_string_get_cstr(preset->name)));
+                    if(preset->frequency) {
+                        if(!subghz_txrx_tx(instance, preset->frequency)) {
+                            FURI_LOG_E(TAG, "Only Rx");
+                            ret = SubGhzTxRxStartTxStateErrorOnlyRx;
+                        }
+                    } 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
+                    subghz_devices_start_async_tx(
+                        instance->radio_device, subghz_transmitter_yield, instance->transmitter);
+                }
+            } else {
+                ret = SubGhzTxRxStartTxStateErrorParserOthers;
+            }
+        } else {
+            ret = SubGhzTxRxStartTxStateErrorParserOthers;
+        }
+        if(ret != SubGhzTxRxStartTxStateOk) {
+            subghz_transmitter_free(instance->transmitter);
+            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);
+    //Todo: Show message
+}
+
+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 = SubGhzHopperStateRunning;
+    }
+    // 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 = SubGhzHopperStateRunning;
+    }
+}
+
+void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->hopper_state == SubGhzHopperStateRunning) {
+        instance->hopper_state = SubGhzHopperStatePause;
+    }
+}
+
+void subghz_txrx_speaker_on(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->debug_pin_state) {
+        subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_ibutton);
+    }
+
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_acquire(30)) {
+            if(!instance->debug_pin_state) {
+                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->debug_pin_state) {
+        subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+    }
+    if(instance->speaker_state != SubGhzSpeakerStateDisable) {
+        if(furi_hal_speaker_is_mine()) {
+            if(!instance->debug_pin_state) {
+                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->debug_pin_state) {
+        subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+    }
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            if(!instance->debug_pin_state) {
+                subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+            }
+        }
+    }
+}
+
+void subghz_txrx_speaker_unmute(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->debug_pin_state) {
+        subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_ibutton);
+    }
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            if(!instance->debug_pin_state) {
+                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_frequency_valid(SubGhzTxRx* instance, uint32_t frequency) {
+    furi_assert(instance);
+    return subghz_devices_is_frequency_valid(instance->radio_device, frequency);
+}
+
+bool subghz_txrx_radio_device_is_tx_allowed(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);
+    subghz_devices_idle(instance->radio_device);
+
+    return ret;
+}
+
+void subghz_txrx_set_debug_pin_state(SubGhzTxRx* instance, bool state) {
+    furi_assert(instance);
+    instance->debug_pin_state = state;
+}
+
+bool subghz_txrx_get_debug_pin_state(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->debug_pin_state;
+}
+
+#ifndef FW_ORIGIN_Official
+void subghz_txrx_reset_dynamic_and_custom_btns(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    subghz_environment_reset_keeloq(instance->environment);
+
+    subghz_custom_btns_reset();
+}
+#endif
+
+SubGhzReceiver* subghz_txrx_get_receiver(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->receiver;
+}

+ 375 - 0
subghz_remote/helpers/txrx/subghz_txrx.h

@@ -0,0 +1,375 @@
+#pragma once
+
+#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;
+
+// Type from subghz_types.h need for txrx working
+/** SubGhzTxRx state */
+typedef enum {
+    SubGhzTxRxStateIDLE,
+    SubGhzTxRxStateRx,
+    SubGhzTxRxStateTx,
+    SubGhzTxRxStateSleep,
+} SubGhzTxRxState;
+
+/** SubGhzHopperState state */
+typedef enum {
+    SubGhzHopperStateOFF,
+    SubGhzHopperStateRunning,
+    SubGhzHopperStatePause,
+    SubGhzHopperStateRSSITimeOut,
+} SubGhzHopperState;
+
+/** SubGhzSpeakerState state */
+typedef enum {
+    SubGhzSpeakerStateDisable,
+    SubGhzSpeakerStateShutdown,
+    SubGhzSpeakerStateEnable,
+} SubGhzSpeakerState;
+
+/** SubGhzRadioDeviceType */
+typedef enum {
+    SubGhzRadioDeviceTypeAuto,
+    SubGhzRadioDeviceTypeInternal,
+    SubGhzRadioDeviceTypeExternalCC1101,
+} SubGhzRadioDeviceType;
+
+/**
+ * Allocate SubGhzTxRx
+ * 
+ * @return SubGhzTxRx* pointer to SubGhzTxRx
+ */
+SubGhzTxRx* subghz_txrx_alloc(void);
+
+/**
+ * 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,
+    bool long_name);
+
+/**
+ * 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_frequency_valid(SubGhzTxRx* instance, uint32_t frequency);
+
+bool subghz_txrx_radio_device_is_tx_allowed(SubGhzTxRx* instance, uint32_t frequency);
+
+void subghz_txrx_set_debug_pin_state(SubGhzTxRx* instance, bool state);
+bool subghz_txrx_get_debug_pin_state(SubGhzTxRx* instance);
+#ifndef FW_ORIGIN_Official
+void subghz_txrx_reset_dynamic_and_custom_btns(SubGhzTxRx* instance);
+#endif
+SubGhzReceiver* subghz_txrx_get_receiver(SubGhzTxRx* instance); // TODO use only in DecodeRaw

+ 31 - 0
subghz_remote/helpers/txrx/subghz_txrx_i.h

@@ -0,0 +1,31 @@
+#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;
+
+    bool debug_pin_state;
+};

BIN
subghz_remote/icon.png


BIN
subghz_remote/icons/DolphinNice_96x59.png


BIN
subghz_remote/icons/remote_scene/Dpad/down.png


BIN
subghz_remote/icons/remote_scene/Dpad/down_hover.png


BIN
subghz_remote/icons/remote_scene/Dpad/left.png


BIN
subghz_remote/icons/remote_scene/Dpad/left_hover.png


BIN
subghz_remote/icons/remote_scene/Dpad/ok.png


BIN
subghz_remote/icons/remote_scene/Dpad/ok_hover.png


BIN
subghz_remote/icons/remote_scene/Dpad/right.png


BIN
subghz_remote/icons/remote_scene/Dpad/right_hover.png


BIN
subghz_remote/icons/remote_scene/Dpad/up.png


BIN
subghz_remote/icons/remote_scene/Dpad/up_hover.png


BIN
subghz_remote/icons/remote_scene/list/ButtonDown_7x4.png


BIN
subghz_remote/icons/remote_scene/list/ButtonLeft_4x7.png


BIN
subghz_remote/icons/remote_scene/list/ButtonRight_4x7.png


BIN
subghz_remote/icons/remote_scene/list/ButtonUp_7x4.png


BIN
subghz_remote/icons/remote_scene/list/Ok_btn_9x9.png


BIN
subghz_remote/icons/remote_scene/statusbar/External_antenna_20x12.png


BIN
subghz_remote/icons/remote_scene/statusbar/Internal_antenna_20x12.png


BIN
subghz_remote/icons/remote_scene/statusbar/Pin_arrow_up_7x9.png


BIN
subghz_remote/icons/remote_scene/statusbar/Status_cube_14x14.png


BIN
subghz_remote/icons/remote_scene/statusbar/status_bar.png


BIN
subghz_remote/icons/sub1_10px.png


BIN
subghz_remote/icons/subrem_10px.png


+ 30 - 0
subghz_remote/scenes/subrem_scene.c

@@ -0,0 +1,30 @@
+#include "../subghz_remote_app_i.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const subrem_scene_on_enter_handlers[])(void*) = {
+#include "subrem_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 subrem_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "subrem_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 subrem_scene_on_exit_handlers[])(void* context) = {
+#include "subrem_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers subrem_scene_handlers = {
+    .on_enter_handlers = subrem_scene_on_enter_handlers,
+    .on_event_handlers = subrem_scene_on_event_handlers,
+    .on_exit_handlers = subrem_scene_on_exit_handlers,
+    .scene_num = SubRemSceneNum,
+};

+ 29 - 0
subghz_remote/scenes/subrem_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) SubRemScene##id,
+typedef enum {
+#include "subrem_scene_config.h"
+    SubRemSceneNum,
+} SubRemScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers subrem_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "subrem_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 "subrem_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 "subrem_scene_config.h"
+#undef ADD_SCENE

+ 12 - 0
subghz_remote/scenes/subrem_scene_config.h

@@ -0,0 +1,12 @@
+ADD_SCENE(subrem, start, Start)
+ADD_SCENE(subrem, open_map_file, OpenMapFile)
+ADD_SCENE(subrem, remote, Remote)
+ADD_SCENE(subrem, edit_menu, EditMenu)
+ADD_SCENE(subrem, edit_submenu, EditSubMenu)
+ADD_SCENE(subrem, edit_label, EditLabel)
+ADD_SCENE(subrem, open_sub_file, OpenSubFile)
+ADD_SCENE(subrem, edit_preview, EditPreview)
+ADD_SCENE(subrem, enter_new_name, EnterNewName)
+#ifdef FW_ORIGIN_Official
+ADD_SCENE(subrem, fw_warning, FwWarning)
+#endif

+ 133 - 0
subghz_remote/scenes/subrem_scene_edit_label.c

@@ -0,0 +1,133 @@
+#include "../subghz_remote_app_i.h"
+
+#include <lib/toolbox/path.h>
+
+typedef enum {
+    SubRemSceneEditLabelStateTextInput,
+    SubRemSceneEditLabelStateWidget,
+} SubRemSceneEditLabelState;
+
+void subrem_scene_edit_label_text_input_callback(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, SubRemCustomEventSceneEditLabelInputDone);
+}
+
+void subrem_scene_edit_label_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    if((result == GuiButtonTypeCenter) && (type == InputTypeShort)) {
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher, SubRemCustomEventSceneEditLabelWidgetAcces);
+    } else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher, SubRemCustomEventSceneEditLabelWidgetBack);
+    }
+}
+
+void subrem_scene_edit_label_on_enter(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub];
+
+    FuriString* temp_str = furi_string_alloc();
+
+    if(furi_string_empty(sub_preset->label)) {
+        if(furi_string_empty(sub_preset->file_path)) {
+            path_extract_filename(sub_preset->file_path, temp_str, true);
+            strcpy(app->file_name_tmp, furi_string_get_cstr(temp_str));
+        } else {
+            strcpy(app->file_name_tmp, "");
+        }
+    } else {
+        strcpy(app->file_name_tmp, furi_string_get_cstr(sub_preset->label));
+    }
+
+    TextInput* text_input = app->text_input;
+    text_input_set_header_text(text_input, "Label name");
+    text_input_set_result_callback(
+        text_input,
+        subrem_scene_edit_label_text_input_callback,
+        app,
+        app->file_name_tmp,
+        25,
+        false);
+#ifndef FW_ORIGIN_Official
+    text_input_set_minimum_length(app->text_input, 0);
+#endif
+    widget_add_string_element(
+        app->widget, 63, 12, AlignCenter, AlignCenter, FontPrimary, "Empty Label Name");
+    widget_add_string_element(
+        app->widget, 63, 32, AlignCenter, AlignCenter, FontSecondary, "Continue?");
+
+    widget_add_button_element(
+        app->widget, GuiButtonTypeCenter, "Ok", subrem_scene_edit_label_widget_callback, app);
+    widget_add_button_element(
+        app->widget, GuiButtonTypeLeft, "Back", subrem_scene_edit_label_widget_callback, app);
+
+    scene_manager_set_scene_state(
+        app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput);
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
+
+    furi_string_free(temp_str);
+}
+
+bool subrem_scene_edit_label_on_event(void* context, SceneManagerEvent event) {
+    SubGhzRemoteApp* app = context;
+
+    FuriString* label = app->map_preset->subs_preset[app->chosen_sub]->label;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        if(scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditLabel) ==
+           SubRemSceneEditLabelStateWidget) {
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput);
+            view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
+            return true;
+        } else if(
+            scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditLabel) ==
+            SubRemSceneEditLabelStateTextInput) {
+            scene_manager_previous_scene(app->scene_manager);
+            return true;
+        }
+
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubRemCustomEventSceneEditLabelInputDone) {
+            if(strcmp(app->file_name_tmp, "") == 0) {
+                scene_manager_set_scene_state(
+                    app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateWidget);
+                view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget);
+
+            } else {
+                furi_string_set(label, app->file_name_tmp);
+                app->map_not_saved = true;
+                scene_manager_previous_scene(app->scene_manager);
+            }
+            return true;
+        } else if(event.event == SubRemCustomEventSceneEditLabelWidgetAcces) {
+            furi_string_set(label, app->file_name_tmp);
+            app->map_not_saved = true;
+            scene_manager_previous_scene(app->scene_manager);
+
+            return true;
+        } else if(event.event == SubRemCustomEventSceneEditLabelWidgetBack) {
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput);
+            view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
+
+            return true;
+        }
+    }
+    return false;
+}
+
+void subrem_scene_edit_label_on_exit(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    // Clear view
+    text_input_reset(app->text_input);
+    widget_reset(app->widget);
+}

+ 123 - 0
subghz_remote/scenes/subrem_scene_edit_menu.c

@@ -0,0 +1,123 @@
+#include "../subghz_remote_app_i.h"
+
+void subrem_scene_edit_menu_callback(SubRemCustomEvent event, void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void subrem_scene_edit_menu_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    if((result == GuiButtonTypeRight) && (type == InputTypeShort)) {
+        app->map_not_saved = false;
+        view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventViewEditMenuBack);
+    } else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
+        view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDEditMenu);
+    }
+}
+
+static uint8_t subrem_scene_edit_menu_state_to_index(SubRemEditMenuState event_id) {
+    uint8_t ret = 0;
+
+    if(event_id == SubRemEditMenuStateUP) {
+        ret = SubRemSubKeyNameUp;
+    } else if(event_id == SubRemEditMenuStateDOWN) {
+        ret = SubRemSubKeyNameDown;
+    } else if(event_id == SubRemEditMenuStateLEFT) {
+        ret = SubRemSubKeyNameLeft;
+    } else if(event_id == SubRemEditMenuStateRIGHT) {
+        ret = SubRemSubKeyNameRight;
+    } else if(event_id == SubRemEditMenuStateOK) {
+        ret = SubRemSubKeyNameOk;
+    }
+
+    return ret;
+}
+
+static void subrem_scene_edit_menu_update_data(SubGhzRemoteApp* app) {
+    furi_assert(app);
+    uint8_t index = subrem_scene_edit_menu_state_to_index(
+        scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditMenu));
+
+    subrem_view_edit_menu_add_data_to_show(
+        app->subrem_edit_menu,
+        index,
+        app->map_preset->subs_preset[index]->label,
+        app->map_preset->subs_preset[index]->file_path,
+        app->map_preset->subs_preset[index]->load_state);
+}
+
+void subrem_scene_edit_menu_on_enter(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    subrem_view_edit_menu_set_callback(
+        app->subrem_edit_menu, subrem_scene_edit_menu_callback, app);
+
+    subrem_scene_edit_menu_update_data(app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDEditMenu);
+
+    Widget* widget = app->widget;
+
+    widget_add_string_element(
+        widget, 63, 12, AlignCenter, AlignBottom, FontPrimary, "Changes are not saved");
+    widget_add_string_element(
+        widget, 63, 32, AlignCenter, AlignBottom, FontPrimary, "do you want to exit?");
+
+    widget_add_button_element(
+        widget, GuiButtonTypeRight, "Yes", subrem_scene_edit_menu_widget_callback, app);
+    widget_add_button_element(
+        widget, GuiButtonTypeLeft, "No", subrem_scene_edit_menu_widget_callback, app);
+}
+
+bool subrem_scene_edit_menu_on_event(void* context, SceneManagerEvent event) {
+    SubGhzRemoteApp* app = context;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        // Catch widget backEvent
+        return true;
+    }
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubRemCustomEventViewEditMenuBack) {
+            if(app->map_not_saved) {
+                view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget);
+            } else if(!scene_manager_search_and_switch_to_previous_scene(
+                          app->scene_manager, SubRemSceneStart)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+
+            return true;
+        } else if(
+            event.event == SubRemCustomEventViewEditMenuUP ||
+            event.event == SubRemCustomEventViewEditMenuDOWN) {
+            scene_manager_set_scene_state(
+                app->scene_manager,
+                SubRemSceneEditMenu,
+                subrem_view_edit_menu_get_index(app->subrem_edit_menu));
+            subrem_scene_edit_menu_update_data(app);
+
+            return true;
+        } else if(event.event == SubRemCustomEventViewEditMenuEdit) {
+            app->chosen_sub = subrem_view_edit_menu_get_index(app->subrem_edit_menu);
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneEditSubMenu, EditSubmenuIndexEditLabel);
+            scene_manager_next_scene(app->scene_manager, SubRemSceneEditSubMenu);
+
+            return true;
+        } else if(event.event == SubRemCustomEventViewEditMenuSave) {
+            scene_manager_next_scene(app->scene_manager, SubRemSceneEditPreview);
+
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void subrem_scene_edit_menu_on_exit(void* context) {
+    SubGhzRemoteApp* app = context;
+    widget_reset(app->widget);
+}

+ 74 - 0
subghz_remote/scenes/subrem_scene_edit_preview.c

@@ -0,0 +1,74 @@
+#include "../subghz_remote_app_i.h"
+#include "../views/remote.h"
+
+#define TAG "SubRemScenRemote"
+
+void subghz_scene_edit_preview_save_popup_callback(void* context) {
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, SubRemCustomEventSceneEditPreviewSaved);
+}
+
+void subrem_scene_edit_preview_callback(SubRemCustomEvent event, void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void subrem_scene_edit_preview_on_enter(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    // Setup view
+    Popup* popup = app->popup;
+    popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
+    popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, app);
+    popup_set_callback(popup, subghz_scene_edit_preview_save_popup_callback);
+    popup_enable_timeout(popup);
+
+    subrem_view_remote_update_data_labels(app->subrem_remote_view, app->map_preset->subs_preset);
+    subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateOFF, 0);
+
+    subrem_view_remote_set_callback(
+        app->subrem_remote_view, subrem_scene_edit_preview_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDRemote);
+}
+
+bool subrem_scene_edit_preview_on_event(void* context, SceneManagerEvent event) {
+    SubGhzRemoteApp* app = context;
+
+    if(event.type == SceneManagerEventTypeBack ||
+       (event.type == SceneManagerEventTypeCustom &&
+        (event.event == SubRemCustomEventViewRemoteStartLEFT ||
+         event.event == SubRemCustomEventViewRemoteForcedStop))) {
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    } else if(
+        event.type == SceneManagerEventTypeCustom &&
+        (event.event == SubRemCustomEventViewRemoteStartRIGHT ||
+         event.event == SubRemCustomEventViewRemoteStartOK)) {
+        if(subrem_save_map_to_file(app)) {
+            view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDPopup);
+            app->map_not_saved = false;
+            return true;
+        }
+        // TODO error screen
+        return true;
+    } else if(
+        event.type == SceneManagerEventTypeCustom &&
+        event.event == SubRemCustomEventSceneEditPreviewSaved) {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, SubRemSceneEditMenu);
+    }
+    // } else if(event.type == SceneManagerEventTypeTick) {
+    // }
+    return false;
+}
+
+void subrem_scene_edit_preview_on_exit(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0);
+    popup_reset(app->popup);
+}

+ 54 - 0
subghz_remote/scenes/subrem_scene_edit_submenu.c

@@ -0,0 +1,54 @@
+#include "../subghz_remote_app_i.h"
+#include "../helpers/subrem_custom_event.h"
+
+void subrem_scene_edit_submenu_text_input_callback(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventSceneEditsubmenu);
+}
+
+void subrem_scene_edit_submenu_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void subrem_scene_edit_submenu_on_enter(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    Submenu* submenu = app->submenu;
+    submenu_add_item(
+        submenu, "Edit Label", EditSubmenuIndexEditLabel, subrem_scene_edit_submenu_callback, app);
+    submenu_add_item(
+        submenu, "Edit File", EditSubmenuIndexEditFile, subrem_scene_edit_submenu_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDSubmenu);
+}
+
+bool subrem_scene_edit_submenu_on_event(void* context, SceneManagerEvent event) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == EditSubmenuIndexEditLabel) {
+            scene_manager_next_scene(app->scene_manager, SubRemSceneEditLabel);
+            consumed = true;
+        } else if(event.event == EditSubmenuIndexEditFile) {
+            scene_manager_next_scene(app->scene_manager, SubRemSceneOpenSubFile);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void subrem_scene_edit_submenu_on_exit(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    submenu_reset(app->submenu);
+}

+ 70 - 0
subghz_remote/scenes/subrem_scene_enter_new_name.c

@@ -0,0 +1,70 @@
+#include "../subghz_remote_app_i.h"
+#include "../helpers/subrem_custom_event.h"
+
+#include <gui/modules/validators.h>
+
+void subrem_scene_enter_new_name_text_input_callback(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventSceneNewName);
+}
+
+void subrem_scene_enter_new_name_on_enter(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    // Setup view
+    TextInput* text_input = app->text_input;
+
+    //strncpy(app->file_name_tmp, "subrem_", SUBREM_MAX_LEN_NAME);
+    text_input_set_header_text(text_input, "Map file Name");
+    text_input_set_result_callback(
+        text_input,
+        subrem_scene_enter_new_name_text_input_callback,
+        app,
+        app->file_name_tmp,
+        25,
+        false);
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        furi_string_get_cstr(app->file_path), SUBREM_APP_EXTENSION, "");
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
+}
+
+bool subrem_scene_enter_new_name_on_event(void* context, SceneManagerEvent event) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubRemCustomEventSceneNewName) {
+            if(strcmp(app->file_name_tmp, "") != 0) {
+                furi_string_set(app->file_path, SUBREM_APP_FOLDER);
+                furi_string_cat_printf(
+                    app->file_path, "/%s%s", app->file_name_tmp, SUBREM_APP_EXTENSION);
+
+                subrem_map_preset_reset(app->map_preset);
+                scene_manager_next_scene(app->scene_manager, SubRemSceneEditMenu);
+            } else { //error
+            }
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void subrem_scene_enter_new_name_on_exit(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    submenu_reset(app->submenu);
+
+    // Clear validator & view
+    void* validator_context = text_input_get_validator_callback_context(app->text_input);
+    text_input_set_validator(app->text_input, NULL, NULL);
+    validator_is_file_free(validator_context);
+    text_input_reset(app->text_input);
+}

+ 129 - 0
subghz_remote/scenes/subrem_scene_fw_warning.c

@@ -0,0 +1,129 @@
+#include "../subghz_remote_app_i.h"
+#include "../helpers/subrem_custom_event.h"
+#ifdef FW_ORIGIN_Official
+typedef enum {
+    SceneFwWarningStateAttention,
+    SceneFwWarningStateAccept,
+} SceneFwWarningState;
+
+static void subrem_scene_fw_warning_widget_render(SubGhzRemoteApp* app, SceneFwWarningState state);
+
+static void
+    subrem_scene_fw_warning_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+
+    if(type == InputTypeShort) {
+        SubRemCustomEvent event = SubRemCustomEventSceneFwWarningExit;
+
+        switch(scene_manager_get_scene_state(app->scene_manager, SubRemSceneFwWarning)) {
+        case SceneFwWarningStateAttention:
+            if(result == GuiButtonTypeRight) {
+                event = SubRemCustomEventSceneFwWarningNext;
+            }
+            break;
+
+        case SceneFwWarningStateAccept:
+            if(result == GuiButtonTypeRight) {
+                event = SubRemCustomEventSceneFwWarningContinue;
+            }
+
+            break;
+        }
+
+        view_dispatcher_send_custom_event(app->view_dispatcher, event);
+    }
+}
+
+static void
+    subrem_scene_fw_warning_widget_render(SubGhzRemoteApp* app, SceneFwWarningState state) {
+    furi_assert(app);
+    Widget* widget = app->widget;
+
+    widget_reset(widget);
+
+    switch(state) {
+    case SceneFwWarningStateAttention:
+        widget_add_button_element(
+            widget, GuiButtonTypeLeft, "Exit", subrem_scene_fw_warning_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Continue", subrem_scene_fw_warning_widget_callback, app);
+        widget_add_string_element(
+            widget, 64, 12, AlignCenter, AlignBottom, FontPrimary, "Not official FW");
+        widget_add_string_multiline_element(
+            widget,
+            64,
+            32,
+            AlignCenter,
+            AlignCenter,
+            FontSecondary,
+            "You are using custom firmware\nPlease download a compatible\nversion of the application");
+        break;
+
+    case SceneFwWarningStateAccept:
+        widget_add_button_element(
+            widget, GuiButtonTypeLeft, "Exit", subrem_scene_fw_warning_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeRight, "Accept", subrem_scene_fw_warning_widget_callback, app);
+        widget_add_string_element(
+            widget, 64, 12, AlignCenter, AlignBottom, FontPrimary, "Not official FW");
+        widget_add_string_multiline_element(
+            widget,
+            64,
+            32,
+            AlignCenter,
+            AlignCenter,
+            FontSecondary,
+            "Yes, I understand that\nthe application can\nbreak my subghz key file");
+        break;
+    }
+}
+
+void subrem_scene_fw_warning_on_enter(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+
+    scene_manager_set_scene_state(
+        app->scene_manager, SubRemSceneFwWarning, SceneFwWarningStateAttention);
+
+    subrem_scene_fw_warning_widget_render(app, SceneFwWarningStateAttention);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget);
+}
+
+bool subrem_scene_fw_warning_on_event(void* context, SceneManagerEvent event) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        consumed = true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubRemCustomEventSceneFwWarningExit) {
+            scene_manager_stop(app->scene_manager);
+            view_dispatcher_stop(app->view_dispatcher);
+            consumed = true;
+        } else if(event.event == SubRemCustomEventSceneFwWarningNext) {
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneFwWarning, SceneFwWarningStateAccept);
+            subrem_scene_fw_warning_widget_render(app, SceneFwWarningStateAccept);
+            view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget);
+            consumed = true;
+        } else if(event.event == SubRemCustomEventSceneFwWarningContinue) {
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void subrem_scene_fw_warning_on_exit(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    widget_reset(app->widget);
+}
+#endif

+ 29 - 0
subghz_remote/scenes/subrem_scene_open_map_file.c

@@ -0,0 +1,29 @@
+#include "../subghz_remote_app_i.h"
+
+void subrem_scene_open_map_file_on_enter(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+
+    SubRemLoadMapState load_state = subrem_load_from_file(app);
+    uint32_t start_scene_state =
+        scene_manager_get_scene_state(app->scene_manager, SubRemSceneStart);
+
+    if(load_state == SubRemLoadMapStateBack) {
+        scene_manager_previous_scene(app->scene_manager);
+    } else if(start_scene_state == SubmenuIndexSubRemEditMapFile) {
+        scene_manager_set_scene_state(app->scene_manager, SubRemSceneEditMenu, SubRemSubKeyNameUp);
+        scene_manager_next_scene(app->scene_manager, SubRemSceneEditMenu);
+    } else if(start_scene_state == SubmenuIndexSubRemOpenMapFile) {
+        scene_manager_next_scene(app->scene_manager, SubRemSceneRemote);
+    }
+}
+
+bool subrem_scene_open_map_file_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void subrem_scene_open_map_file_on_exit(void* context) {
+    UNUSED(context);
+}

+ 119 - 0
subghz_remote/scenes/subrem_scene_open_sub_file.c

@@ -0,0 +1,119 @@
+#include "../subghz_remote_app_i.h"
+
+void subrem_scene_open_sub_file_error_popup_callback(void* context) {
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, SubRemCustomEventSceneEditOpenSubErrorPopup);
+}
+
+SubRemLoadSubState subrem_scene_open_sub_file_dialog(SubGhzRemoteApp* app) {
+    furi_assert(app);
+
+    SubRemSubFilePreset* sub = app->map_preset->subs_preset[app->chosen_sub];
+
+    FuriString* temp_file_path = furi_string_alloc();
+
+    if(furi_string_empty(sub->file_path)) {
+        furi_string_set(temp_file_path, SUBGHZ_RAW_FOLDER);
+    } else {
+        furi_string_set(temp_file_path, sub->file_path);
+    }
+
+    SubRemLoadSubState ret = SubRemLoadSubStateNotSet;
+
+    DialogsFileBrowserOptions browser_options;
+
+    dialog_file_browser_set_basic_options(
+        &browser_options, SUBGHZ_APP_FILENAME_EXTENSION, &I_sub1_10px);
+    browser_options.base_path = SUBGHZ_RAW_FOLDER;
+
+    // Input events and views are managed by file_select
+    if(!dialog_file_browser_show(app->dialogs, temp_file_path, temp_file_path, &browser_options)) {
+    } else {
+        // Check sub file
+        SubRemSubFilePreset* sub_candidate = subrem_sub_file_preset_alloc();
+        furi_string_set(sub_candidate->label, sub->label);
+        furi_string_set(sub_candidate->file_path, temp_file_path);
+
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+        if(flipper_format_file_open_existing(
+               fff_file, furi_string_get_cstr(sub_candidate->file_path))) {
+            ret = subrem_sub_preset_load(sub_candidate, app->txrx, fff_file);
+        }
+
+        flipper_format_file_close(fff_file);
+        flipper_format_free(fff_file);
+        furi_record_close(RECORD_STORAGE);
+
+        if(ret == SubRemLoadSubStateOK) {
+            subrem_sub_file_preset_free(app->map_preset->subs_preset[app->chosen_sub]);
+            app->map_preset->subs_preset[app->chosen_sub] = sub_candidate;
+            app->map_not_saved = true;
+        } else {
+            subrem_sub_file_preset_free(sub_candidate);
+        }
+    }
+
+    furi_string_free(temp_file_path);
+
+    return ret;
+}
+
+void subrem_scene_open_sub_file_on_enter(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+
+    SubRemLoadSubState load_state = subrem_scene_open_sub_file_dialog(app);
+
+    Popup* popup = app->popup;
+    // popup_set_icon();
+    popup_set_header(popup, "ERROR", 63, 16, AlignCenter, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, app);
+    popup_set_callback(popup, subrem_scene_open_sub_file_error_popup_callback);
+    popup_enable_timeout(popup);
+
+    if(load_state == SubRemLoadSubStateOK) {
+        scene_manager_previous_scene(app->scene_manager);
+    } else if(load_state == SubRemLoadSubStateNotSet) {
+        scene_manager_previous_scene(app->scene_manager);
+    } else {
+        switch(load_state) {
+        case SubRemLoadSubStateErrorFreq:
+
+            popup_set_text(popup, "Bad frequency", 63, 30, AlignCenter, AlignBottom);
+            break;
+        case SubRemLoadSubStateErrorMod:
+
+            popup_set_text(popup, "Bad modulation", 63, 30, AlignCenter, AlignBottom);
+            break;
+        case SubRemLoadSubStateErrorProtocol:
+
+            popup_set_text(popup, "Unsupported protocol", 63, 30, AlignCenter, AlignBottom);
+            break;
+
+        default:
+            break;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDPopup);
+    }
+}
+
+bool subrem_scene_open_sub_file_on_event(void* context, SceneManagerEvent event) {
+    SubGhzRemoteApp* app = context;
+
+    if(event.type == SceneManagerEventTypeCustom &&
+       event.event == SubRemCustomEventSceneEditOpenSubErrorPopup) {
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    }
+    return false;
+}
+
+void subrem_scene_open_sub_file_on_exit(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    popup_reset(app->popup);
+}

+ 118 - 0
subghz_remote/scenes/subrem_scene_remote.c

@@ -0,0 +1,118 @@
+#include "../subghz_remote_app_i.h"
+#include "../views/remote.h"
+
+#include <lib/subghz/protocols/raw.h>
+
+#define TAG "SubRemScenRemote"
+
+void subrem_scene_remote_callback(SubRemCustomEvent event, void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void subrem_scene_remote_raw_callback_end_tx(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventViewRemoteForcedStop);
+}
+
+static uint8_t subrem_scene_remote_event_to_index(SubRemCustomEvent event_id) {
+    uint8_t ret = 0;
+
+    if(event_id == SubRemCustomEventViewRemoteStartUP) {
+        ret = SubRemSubKeyNameUp;
+    } else if(event_id == SubRemCustomEventViewRemoteStartDOWN) {
+        ret = SubRemSubKeyNameDown;
+    } else if(event_id == SubRemCustomEventViewRemoteStartLEFT) {
+        ret = SubRemSubKeyNameLeft;
+    } else if(event_id == SubRemCustomEventViewRemoteStartRIGHT) {
+        ret = SubRemSubKeyNameRight;
+    } else if(event_id == SubRemCustomEventViewRemoteStartOK) {
+        ret = SubRemSubKeyNameOk;
+    }
+
+    return ret;
+}
+
+void subrem_scene_remote_on_enter(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    subrem_view_remote_update_data_labels(app->subrem_remote_view, app->map_preset->subs_preset);
+    subrem_view_remote_set_radio(
+        app->subrem_remote_view,
+        subghz_txrx_radio_device_get(app->txrx) != SubGhzRadioDeviceTypeInternal);
+
+    subrem_view_remote_set_callback(app->subrem_remote_view, subrem_scene_remote_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDRemote);
+}
+
+bool subrem_scene_remote_on_event(void* context, SceneManagerEvent event) {
+    SubGhzRemoteApp* app = context;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubRemCustomEventViewRemoteBack) {
+            if(!scene_manager_previous_scene(app->scene_manager)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            return true;
+        } else if(
+            event.event == SubRemCustomEventViewRemoteStartUP ||
+            event.event == SubRemCustomEventViewRemoteStartDOWN ||
+            event.event == SubRemCustomEventViewRemoteStartLEFT ||
+            event.event == SubRemCustomEventViewRemoteStartRIGHT ||
+            event.event == SubRemCustomEventViewRemoteStartOK) {
+            // Start sending sub
+            subrem_tx_stop_sub(app, true);
+
+            uint8_t chosen_sub = subrem_scene_remote_event_to_index(event.event);
+            app->chosen_sub = chosen_sub;
+
+            subrem_view_remote_set_state(
+                app->subrem_remote_view, SubRemViewRemoteStateLoading, chosen_sub);
+
+            if(subrem_tx_start_sub(app, app->map_preset->subs_preset[chosen_sub])) {
+                if(app->map_preset->subs_preset[chosen_sub]->type == SubGhzProtocolTypeRAW) {
+                    subghz_txrx_set_raw_file_encoder_worker_callback_end(
+                        app->txrx, subrem_scene_remote_raw_callback_end_tx, app);
+                }
+                subrem_view_remote_set_state(
+                    app->subrem_remote_view, SubRemViewRemoteStateSending, chosen_sub);
+                notification_message(app->notifications, &sequence_blink_start_magenta);
+            } else {
+                subrem_view_remote_set_state(
+                    app->subrem_remote_view, SubRemViewRemoteStateIdle, 0);
+                notification_message(app->notifications, &sequence_blink_red_100);
+            }
+            return true;
+        } else if(event.event == SubRemCustomEventViewRemoteForcedStop) {
+            subrem_tx_stop_sub(app, true);
+            subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0);
+
+            notification_message(app->notifications, &sequence_blink_stop);
+            return true;
+        } else if(event.event == SubRemCustomEventViewRemoteStop) {
+            if(subrem_tx_stop_sub(app, false)) {
+                subrem_view_remote_set_state(
+                    app->subrem_remote_view, SubRemViewRemoteStateIdle, 0);
+
+                notification_message(app->notifications, &sequence_blink_stop);
+            }
+            return true;
+        }
+    }
+    // } else if(event.type == SceneManagerEventTypeTick) {
+    // }
+    return false;
+}
+
+void subrem_scene_remote_on_exit(void* context) {
+    SubGhzRemoteApp* app = context;
+
+    subrem_tx_stop_sub(app, true);
+
+    subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0);
+
+    notification_message(app->notifications, &sequence_blink_stop);
+}

+ 100 - 0
subghz_remote/scenes/subrem_scene_start.c

@@ -0,0 +1,100 @@
+#include "../subghz_remote_app_i.h"
+#include "../helpers/subrem_custom_event.h"
+
+void subrem_scene_start_submenu_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void subrem_scene_start_on_enter(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    Submenu* submenu = app->submenu;
+    submenu_add_item(
+        submenu,
+        "Open Map File",
+        SubmenuIndexSubRemOpenMapFile,
+        subrem_scene_start_submenu_callback,
+        app);
+#if FURI_DEBUG
+    submenu_add_item(
+        submenu,
+        "Remote_Debug",
+        SubmenuIndexSubRemRemoteView,
+        subrem_scene_start_submenu_callback,
+        app);
+#endif
+    submenu_add_item(
+        submenu,
+        "Edit Map File",
+        SubmenuIndexSubRemEditMapFile,
+        subrem_scene_start_submenu_callback,
+        app);
+    submenu_add_item(
+        submenu,
+        "New Map File",
+        SubmenuIndexSubRemNewMapFile,
+        subrem_scene_start_submenu_callback,
+        app);
+    // submenu_add_item(
+    //     submenu,
+    //     "About",
+    //     SubmenuIndexSubGhzRemoteAbout,
+    //     subrem_scene_start_submenu_callback,
+    //     app);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(app->scene_manager, SubRemSceneStart));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDSubmenu);
+}
+
+bool subrem_scene_start_on_event(void* context, SceneManagerEvent event) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexSubRemOpenMapFile) {
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneStart, SubmenuIndexSubRemOpenMapFile);
+
+            scene_manager_next_scene(app->scene_manager, SubRemSceneOpenMapFile);
+            consumed = true;
+        }
+#if FURI_DEBUG
+        else if(event.event == SubmenuIndexSubRemRemoteView) {
+            scene_manager_next_scene(app->scene_manager, SubRemSceneRemote);
+            consumed = true;
+        }
+#endif
+        else if(event.event == SubmenuIndexSubRemEditMapFile) {
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneStart, SubmenuIndexSubRemEditMapFile);
+            scene_manager_next_scene(app->scene_manager, SubRemSceneOpenMapFile);
+            consumed = true;
+        } else if(event.event == SubmenuIndexSubRemNewMapFile) {
+            scene_manager_set_scene_state(
+                app->scene_manager, SubRemSceneStart, SubmenuIndexSubRemNewMapFile);
+            scene_manager_next_scene(app->scene_manager, SubRemSceneEnterNewName);
+            consumed = true;
+        }
+        // } else if(event.event == SubmenuIndexSubRemAbout) {
+        //     scene_manager_next_scene(app->scene_manager, SubRemSceneAbout);
+        //     consumed = true;
+        // }
+    }
+
+    return consumed;
+}
+
+void subrem_scene_start_on_exit(void* context) {
+    furi_assert(context);
+
+    SubGhzRemoteApp* app = context;
+    submenu_reset(app->submenu);
+}

+ 218 - 0
subghz_remote/subghz_remote_app.c

@@ -0,0 +1,218 @@
+#include "subghz_remote_app_i.h"
+#include <lib/toolbox/version.h>
+
+static bool subghz_remote_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool subghz_remote_app_back_event_callback(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void subghz_remote_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+static void subghz_remote_make_app_folder(SubGhzRemoteApp* app) {
+    furi_assert(app);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    // Migrate old users data
+    storage_common_migrate(storage, EXT_PATH("unirf"), SUBREM_APP_FOLDER);
+
+    if(!storage_simply_mkdir(storage, SUBREM_APP_FOLDER)) {
+        // FURI_LOG_E(TAG, "Could not create folder %s", SUBREM_APP_FOLDER);
+        dialog_message_show_storage_error(app->dialogs, "Cannot create\napp folder");
+    }
+    furi_record_close(RECORD_STORAGE);
+}
+
+SubGhzRemoteApp* subghz_remote_app_alloc() {
+    SubGhzRemoteApp* app = malloc(sizeof(SubGhzRemoteApp));
+
+    furi_hal_power_suppress_charge_enter();
+
+    app->file_path = furi_string_alloc();
+    furi_string_set(app->file_path, SUBREM_APP_FOLDER);
+
+    // GUI
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // View Dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+
+    app->scene_manager = scene_manager_alloc(&subrem_scene_handlers, app);
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, subghz_remote_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, subghz_remote_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, subghz_remote_app_tick_event_callback, 100);
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // SubMenu
+    app->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, SubRemViewIDSubmenu, submenu_get_view(app->submenu));
+
+    // Dialog
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    // TextInput
+    app->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, SubRemViewIDTextInput, text_input_get_view(app->text_input));
+
+    // Widget
+    app->widget = widget_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, SubRemViewIDWidget, widget_get_view(app->widget));
+
+    // Popup
+    app->popup = popup_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, SubRemViewIDPopup, popup_get_view(app->popup));
+
+    // Remote view
+    app->subrem_remote_view = subrem_view_remote_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        SubRemViewIDRemote,
+        subrem_view_remote_get_view(app->subrem_remote_view));
+
+    // Edit Menu view
+    app->subrem_edit_menu = subrem_view_edit_menu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        SubRemViewIDEditMenu,
+        subrem_view_edit_menu_get_view(app->subrem_edit_menu));
+
+    app->map_preset = malloc(sizeof(SubRemMapPreset));
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        app->map_preset->subs_preset[i] = subrem_sub_file_preset_alloc();
+    }
+
+    app->txrx = subghz_txrx_alloc();
+
+    subghz_txrx_set_need_save_callback(app->txrx, subrem_save_active_sub, app);
+
+    app->map_not_saved = false;
+
+    return app;
+}
+
+void subghz_remote_app_free(SubGhzRemoteApp* app) {
+    furi_assert(app);
+
+    furi_hal_power_suppress_charge_exit();
+
+    // Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDSubmenu);
+    submenu_free(app->submenu);
+
+    // Dialog
+    furi_record_close(RECORD_DIALOGS);
+
+    // TextInput
+    view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDTextInput);
+    text_input_free(app->text_input);
+
+    // Widget
+    view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDWidget);
+    widget_free(app->widget);
+
+    // Popup
+    view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDPopup);
+    popup_free(app->popup);
+
+    // Remote view
+    view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDRemote);
+    subrem_view_remote_free(app->subrem_remote_view);
+
+    // Edit view
+    view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDEditMenu);
+    subrem_view_edit_menu_free(app->subrem_edit_menu);
+
+    scene_manager_free(app->scene_manager);
+    view_dispatcher_free(app->view_dispatcher);
+
+    subghz_txrx_free(app->txrx);
+
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        subrem_sub_file_preset_free(app->map_preset->subs_preset[i]);
+    }
+    free(app->map_preset);
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    app->notifications = NULL;
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+
+    // Path strings
+    furi_string_free(app->file_path);
+
+    free(app);
+}
+
+int32_t subghz_remote_app(void* arg) {
+    SubGhzRemoteApp* subghz_remote_app = subghz_remote_app_alloc();
+
+    subghz_remote_make_app_folder(subghz_remote_app);
+
+    bool map_loaded = false;
+#ifdef FW_ORIGIN_Official
+    const bool fw_ofw = strcmp(version_get_firmware_origin(version_get()), "Official") == 0;
+#endif
+    if((arg != NULL) && (strlen(arg) != 0)) {
+        furi_string_set(subghz_remote_app->file_path, (const char*)arg);
+        SubRemLoadMapState load_state = subrem_map_file_load(
+            subghz_remote_app, furi_string_get_cstr(subghz_remote_app->file_path));
+
+        if(load_state == SubRemLoadMapStateOK || load_state == SubRemLoadMapStateNotAllOK) {
+            map_loaded = true;
+        } else {
+            // TODO Replace
+            dialog_message_show_storage_error(subghz_remote_app->dialogs, "Cannot load\nmap file");
+        }
+    }
+
+    if(map_loaded) {
+        scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneRemote);
+    } else {
+        furi_string_set(subghz_remote_app->file_path, SUBREM_APP_FOLDER);
+        scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneStart);
+#ifdef FW_ORIGIN_Official
+        if(fw_ofw) {
+            scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneOpenMapFile);
+        }
+    }
+
+    if(!fw_ofw) {
+        scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneFwWarning);
+    }
+#else
+        scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneOpenMapFile);
+    }
+#endif
+
+    view_dispatcher_run(subghz_remote_app->view_dispatcher);
+
+    subghz_remote_app_free(subghz_remote_app);
+
+    return 0;
+}

+ 317 - 0
subghz_remote/subghz_remote_app_i.c

@@ -0,0 +1,317 @@
+#include "subghz_remote_app_i.h"
+#include <lib/toolbox/path.h>
+#include <flipper_format/flipper_format_i.h>
+
+#include "helpers/txrx/subghz_txrx.h"
+#ifndef FW_ORIGIN_Official
+#include <lib/subghz/blocks/custom_btn.h>
+#endif
+
+#define TAG "SubGhzRemote"
+
+static const char* map_file_labels[SubRemSubKeyNameMaxCount][2] = {
+    [SubRemSubKeyNameUp] = {"UP", "ULABEL"},
+    [SubRemSubKeyNameDown] = {"DOWN", "DLABEL"},
+    [SubRemSubKeyNameLeft] = {"LEFT", "LLABEL"},
+    [SubRemSubKeyNameRight] = {"RIGHT", "RLABEL"},
+    [SubRemSubKeyNameOk] = {"OK", "OKLABEL"},
+};
+
+void subrem_map_preset_reset(SubRemMapPreset* map_preset) {
+    furi_assert(map_preset);
+
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        subrem_sub_file_preset_reset(map_preset->subs_preset[i]);
+    }
+}
+
+static SubRemLoadMapState subrem_map_preset_check(
+    SubRemMapPreset* map_preset,
+    SubGhzTxRx* txrx,
+    FlipperFormat* fff_data_file) {
+    furi_assert(map_preset);
+    furi_assert(txrx);
+
+    bool all_loaded = true;
+    SubRemLoadMapState ret = SubRemLoadMapStateErrorBrokenFile;
+
+    SubRemLoadSubState sub_loadig_state;
+    SubRemSubFilePreset* sub_preset;
+
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        sub_preset = map_preset->subs_preset[i];
+
+        sub_loadig_state = SubRemLoadSubStateErrorNoFile;
+
+        if(furi_string_empty(sub_preset->file_path)) {
+            // FURI_LOG_I(TAG, "Empty file path");
+        } else if(!flipper_format_file_open_existing(
+                      fff_data_file, furi_string_get_cstr(sub_preset->file_path))) {
+            sub_preset->load_state = SubRemLoadSubStateErrorNoFile;
+            FURI_LOG_W(TAG, "Error open file %s", furi_string_get_cstr(sub_preset->file_path));
+        } else {
+            sub_loadig_state = subrem_sub_preset_load(sub_preset, txrx, fff_data_file);
+        }
+
+        if(sub_loadig_state != SubRemLoadSubStateOK) {
+            all_loaded = false;
+        } else {
+            ret = SubRemLoadMapStateNotAllOK;
+        }
+
+        if(ret != SubRemLoadMapStateErrorBrokenFile && all_loaded) {
+            ret = SubRemLoadMapStateOK;
+        }
+
+        flipper_format_file_close(fff_data_file);
+    }
+
+    return ret;
+}
+
+static bool subrem_map_preset_load(SubRemMapPreset* map_preset, FlipperFormat* fff_data_file) {
+    furi_assert(map_preset);
+    bool ret = false;
+    SubRemSubFilePreset* sub_preset;
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        sub_preset = map_preset->subs_preset[i];
+        if(!flipper_format_read_string(
+               fff_data_file, map_file_labels[i][0], sub_preset->file_path)) {
+#if FURI_DEBUG
+            FURI_LOG_W(TAG, "No file patch for %s", map_file_labels[i][0]);
+#endif
+            sub_preset->type = SubGhzProtocolTypeUnknown;
+        } else if(!path_contains_only_ascii(furi_string_get_cstr(sub_preset->file_path))) {
+            FURI_LOG_E(TAG, "Incorrect characters in [%s] file path", map_file_labels[i][0]);
+            sub_preset->type = SubGhzProtocolTypeUnknown;
+        } else if(!flipper_format_rewind(fff_data_file)) {
+            // Rewind error
+        } else if(!flipper_format_read_string(
+                      fff_data_file, map_file_labels[i][1], sub_preset->label)) {
+#if FURI_DEBUG
+            FURI_LOG_W(TAG, "No Label for %s", map_file_labels[i][0]);
+#endif
+            ret = true;
+        } else {
+            ret = true;
+        }
+        if(ret) {
+            // Preload seccesful
+            FURI_LOG_I(
+                TAG,
+                "%-5s: %s %s",
+                map_file_labels[i][0],
+                furi_string_get_cstr(sub_preset->label),
+                furi_string_get_cstr(sub_preset->file_path));
+            sub_preset->load_state = SubRemLoadSubStatePreloaded;
+        }
+
+        flipper_format_rewind(fff_data_file);
+    }
+    return ret;
+}
+
+SubRemLoadMapState subrem_map_file_load(SubGhzRemoteApp* app, const char* file_path) {
+    furi_assert(app);
+    furi_assert(file_path);
+#if FURI_DEBUG
+    FURI_LOG_I(TAG, "Load Map File Start");
+#endif
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
+    SubRemLoadMapState ret = SubRemLoadMapStateErrorOpenError;
+#if FURI_DEBUG
+    FURI_LOG_I(TAG, "Open Map File..");
+#endif
+    subrem_map_preset_reset(app->map_preset);
+
+    if(!flipper_format_file_open_existing(fff_data_file, file_path)) {
+        FURI_LOG_E(TAG, "Could not open MAP file %s", file_path);
+        ret = SubRemLoadMapStateErrorOpenError;
+    } else {
+        if(!subrem_map_preset_load(app->map_preset, fff_data_file)) {
+            FURI_LOG_E(TAG, "Could no Sub file path in MAP file");
+            // ret = // error for popup
+        } else if(!flipper_format_file_close(fff_data_file)) {
+            ret = SubRemLoadMapStateErrorOpenError;
+        } else {
+            ret = subrem_map_preset_check(app->map_preset, app->txrx, fff_data_file);
+        }
+    }
+
+    if(ret == SubRemLoadMapStateOK) {
+        FURI_LOG_I(TAG, "Load Map File Seccesful");
+    } else if(ret == SubRemLoadMapStateNotAllOK) {
+        FURI_LOG_I(TAG, "Load Map File Seccesful [Not all files]");
+    } else {
+        FURI_LOG_E(TAG, "Broken Map File");
+    }
+
+    flipper_format_file_close(fff_data_file);
+    flipper_format_free(fff_data_file);
+
+    furi_record_close(RECORD_STORAGE);
+    return ret;
+}
+
+bool subrem_save_protocol_to_file(FlipperFormat* flipper_format, const char* sub_file_name) {
+    furi_assert(flipper_format);
+    furi_assert(sub_file_name);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    Stream* flipper_format_stream = flipper_format_get_raw_stream(flipper_format);
+
+    bool saved = false;
+    uint32_t repeat = 200;
+    FuriString* file_dir = furi_string_alloc();
+
+    path_extract_dirname(sub_file_name, file_dir);
+    do {
+        // removing additional fields
+        flipper_format_delete_key(flipper_format, "Repeat");
+        // flipper_format_delete_key(flipper_format, "Manufacture");
+
+        if(!storage_simply_remove(storage, sub_file_name)) {
+            break;
+        }
+
+        //ToDo check Write
+        stream_seek(flipper_format_stream, 0, StreamOffsetFromStart);
+        stream_save_to_file(flipper_format_stream, storage, sub_file_name, FSOM_CREATE_ALWAYS);
+
+        if(!flipper_format_insert_or_update_uint32(flipper_format, "Repeat", &repeat, 1)) {
+            FURI_LOG_E(TAG, "Unable Repeat");
+            break;
+        }
+
+        saved = true;
+    } while(0);
+
+    furi_string_free(file_dir);
+    furi_record_close(RECORD_STORAGE);
+    return saved;
+}
+
+void subrem_save_active_sub(void* context) {
+    furi_assert(context);
+    SubGhzRemoteApp* app = context;
+
+    SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub];
+    subrem_save_protocol_to_file(
+        sub_preset->fff_data, furi_string_get_cstr(sub_preset->file_path));
+}
+
+bool subrem_tx_start_sub(SubGhzRemoteApp* app, SubRemSubFilePreset* sub_preset) {
+    furi_assert(app);
+    furi_assert(sub_preset);
+    bool ret = false;
+
+    subrem_tx_stop_sub(app, true);
+
+    if(sub_preset->type == SubGhzProtocolTypeUnknown) {
+        ret = false;
+    } else {
+        FURI_LOG_I(TAG, "Send %s", furi_string_get_cstr(sub_preset->label));
+
+        subghz_txrx_load_decoder_by_name_protocol(
+            app->txrx, furi_string_get_cstr(sub_preset->protocaol_name));
+
+        subghz_txrx_set_preset(
+            app->txrx,
+            furi_string_get_cstr(sub_preset->freq_preset.name),
+            sub_preset->freq_preset.frequency,
+            NULL,
+            0);
+#ifndef FW_ORIGIN_Official
+        subghz_custom_btns_reset();
+#endif
+        if(subghz_txrx_tx_start(app->txrx, sub_preset->fff_data) == SubGhzTxRxStartTxStateOk) {
+            ret = true;
+        }
+    }
+
+    return ret;
+}
+
+bool subrem_tx_stop_sub(SubGhzRemoteApp* app, bool forced) {
+    furi_assert(app);
+    SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub];
+
+    if(forced || (sub_preset->type != SubGhzProtocolTypeRAW)) {
+        subghz_txrx_stop(app->txrx);
+#ifndef FW_ORIGIN_Official
+        if(sub_preset->type == SubGhzProtocolTypeDynamic) {
+            subghz_txrx_reset_dynamic_and_custom_btns(app->txrx);
+        }
+        subghz_custom_btns_reset();
+#endif
+        return true;
+    }
+
+    return false;
+}
+
+SubRemLoadMapState subrem_load_from_file(SubGhzRemoteApp* app) {
+    furi_assert(app);
+
+    FuriString* file_path = furi_string_alloc();
+    SubRemLoadMapState ret = SubRemLoadMapStateBack;
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, SUBREM_APP_EXTENSION, &I_subrem_10px);
+    browser_options.base_path = SUBREM_APP_FOLDER;
+
+    // Input events and views are managed by file_select
+    if(!dialog_file_browser_show(app->dialogs, app->file_path, app->file_path, &browser_options)) {
+    } else {
+        ret = subrem_map_file_load(app, furi_string_get_cstr(app->file_path));
+    }
+
+    furi_string_free(file_path);
+
+    return ret;
+}
+
+bool subrem_save_map_to_file(SubGhzRemoteApp* app) {
+    furi_assert(app);
+
+    const char* file_name = furi_string_get_cstr(app->file_path);
+    bool saved = false;
+    FlipperFormat* fff_data = flipper_format_string_alloc();
+
+    SubRemSubFilePreset* sub_preset;
+
+    flipper_format_write_header_cstr(
+        fff_data, SUBREM_APP_APP_FILE_TYPE, SUBREM_APP_APP_FILE_VERSION);
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        sub_preset = app->map_preset->subs_preset[i];
+        if(!furi_string_empty(sub_preset->file_path)) {
+            flipper_format_write_string(fff_data, map_file_labels[i][0], sub_preset->file_path);
+        }
+    }
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        sub_preset = app->map_preset->subs_preset[i];
+        if(!furi_string_empty(sub_preset->file_path)) {
+            flipper_format_write_string(fff_data, map_file_labels[i][1], sub_preset->label);
+        }
+    }
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    Stream* flipper_format_stream = flipper_format_get_raw_stream(fff_data);
+
+    do {
+        if(!storage_simply_remove(storage, file_name)) {
+            break;
+        }
+        //ToDo check Write
+        stream_seek(flipper_format_stream, 0, StreamOffsetFromStart);
+        stream_save_to_file(flipper_format_stream, storage, file_name, FSOM_CREATE_ALWAYS);
+
+        saved = true;
+    } while(0);
+
+    furi_record_close(RECORD_STORAGE);
+    flipper_format_free(fff_data);
+
+    return saved;
+}

+ 68 - 0
subghz_remote/subghz_remote_app_i.h

@@ -0,0 +1,68 @@
+#pragma once
+
+#include "helpers/subrem_types.h"
+#include "helpers/subrem_presets.h"
+#include "scenes/subrem_scene.h"
+
+#include "helpers/txrx/subghz_txrx.h"
+
+#include "subghz_remote_icons.h"
+
+#include "views/remote.h"
+#include "views/edit_menu.h"
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/popup.h>
+#include <dialogs/dialogs.h>
+#include <notification/notification_messages.h>
+#include <storage/storage.h>
+
+#include <flipper_format/flipper_format_i.h>
+
+#define SUBREM_APP_FOLDER EXT_PATH("subghz_remote")
+#define SUBREM_MAX_LEN_NAME 64
+
+typedef struct {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    NotificationApp* notifications;
+    DialogsApp* dialogs;
+    Widget* widget;
+    Popup* popup;
+    TextInput* text_input;
+    Submenu* submenu;
+
+    FuriString* file_path;
+    char file_name_tmp[SUBREM_MAX_LEN_NAME];
+
+    SubRemViewRemote* subrem_remote_view;
+    SubRemViewEditMenu* subrem_edit_menu;
+
+    SubRemMapPreset* map_preset;
+
+    SubGhzTxRx* txrx;
+
+    bool map_not_saved;
+
+    uint8_t chosen_sub;
+} SubGhzRemoteApp;
+
+SubRemLoadMapState subrem_load_from_file(SubGhzRemoteApp* app);
+
+bool subrem_tx_start_sub(SubGhzRemoteApp* app, SubRemSubFilePreset* sub_preset);
+
+bool subrem_tx_stop_sub(SubGhzRemoteApp* app, bool forced);
+
+SubRemLoadMapState subrem_map_file_load(SubGhzRemoteApp* app, const char* file_path);
+
+void subrem_map_preset_reset(SubRemMapPreset* map_preset);
+
+bool subrem_save_map_to_file(SubGhzRemoteApp* app);
+
+void subrem_save_active_sub(void* context);

+ 290 - 0
subghz_remote/views/edit_menu.c

@@ -0,0 +1,290 @@
+#include "edit_menu.h"
+#include "../subghz_remote_app_i.h"
+
+#include <input/input.h>
+#include <gui/elements.h>
+
+#define subrem_view_edit_menu_MAX_LABEL_LENGTH 12
+
+#define FRAME_HEIGHT 12
+
+struct SubRemViewEditMenu {
+    View* view;
+    SubRemViewEditMenuCallback callback;
+    void* context;
+};
+
+typedef struct {
+    FuriString* label;
+    FuriString* file_path;
+    SubRemLoadSubState sub_state;
+
+    uint8_t chosen;
+} SubRemViewEditMenuModel;
+
+void subrem_view_edit_menu_set_callback(
+    SubRemViewEditMenu* subrem_view_edit_menu,
+    SubRemViewEditMenuCallback callback,
+    void* context) {
+    furi_assert(subrem_view_edit_menu);
+
+    subrem_view_edit_menu->callback = callback;
+    subrem_view_edit_menu->context = context;
+}
+
+void subrem_view_edit_menu_add_data_to_show(
+    SubRemViewEditMenu* subrem_view_edit_remote,
+    uint8_t index,
+    FuriString* label,
+    FuriString* path,
+    SubRemLoadSubState state) {
+    furi_assert(subrem_view_edit_remote);
+
+    with_view_model(
+        subrem_view_edit_remote->view,
+        SubRemViewEditMenuModel * model,
+        {
+            model->chosen = index;
+            if(!furi_string_empty(label)) {
+                furi_string_set(model->label, label);
+            } else {
+                furi_string_set(model->label, "Empty label");
+            }
+            furi_string_set(model->file_path, path);
+            model->sub_state = state;
+        },
+        true);
+}
+
+uint8_t subrem_view_edit_menu_get_index(SubRemViewEditMenu* subrem_view_edit_remote) {
+    furi_assert(subrem_view_edit_remote);
+    uint8_t index;
+
+    with_view_model(
+        subrem_view_edit_remote->view,
+        SubRemViewEditMenuModel * model,
+        { index = model->chosen; },
+        true);
+    return index;
+}
+
+void subrem_view_edit_menu_draw(Canvas* canvas, SubRemViewEditMenuModel* model) {
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_clear(canvas);
+
+    // Draw bottom btn
+    canvas_set_font(canvas, FontSecondary);
+    elements_button_left(canvas, "Back");
+    elements_button_center(canvas, "Edit");
+    elements_button_right(canvas, "Save");
+
+    // Draw top frame
+    canvas_draw_line(canvas, 1, 0, 125, 0);
+    canvas_draw_box(canvas, 0, 1, 127, FRAME_HEIGHT - 2);
+    canvas_draw_line(canvas, 1, FRAME_HEIGHT - 1, 125, FRAME_HEIGHT - 1);
+
+    canvas_set_color(canvas, ColorWhite);
+
+    // Draw btn name
+    canvas_set_font(canvas, FontPrimary);
+    switch(model->chosen) {
+    case SubRemSubKeyNameUp:
+        canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "UP");
+        break;
+
+    case SubRemSubKeyNameDown:
+        canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "DOWN");
+        break;
+
+    case SubRemSubKeyNameLeft:
+        canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "LEFT");
+        break;
+
+    case SubRemSubKeyNameRight:
+        canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "RIGHT");
+        break;
+
+    case SubRemSubKeyNameOk:
+        canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "OK");
+        break;
+
+    default:
+        break;
+    }
+
+    // Draw Label
+    canvas_set_font(canvas, FontSecondary);
+    elements_text_box(
+        canvas,
+        38,
+        2,
+        127 - 38,
+        FRAME_HEIGHT,
+        AlignCenter,
+        AlignBottom,
+        furi_string_empty(model->label) ? "Empty label" : furi_string_get_cstr(model->label),
+        true);
+
+    // Draw arrow
+    canvas_set_color(canvas, ColorBlack);
+    if(model->chosen != 0) {
+        canvas_draw_icon(canvas, 119, 13, &I_Pin_arrow_up_7x9);
+    }
+    if(model->chosen != 4) {
+        canvas_draw_icon_ex(canvas, 119, 42, &I_Pin_arrow_up_7x9, IconRotation180);
+    }
+
+    // Draw file_path
+    if(model->sub_state == SubRemLoadSubStateOK) {
+        canvas_set_font(canvas, FontSecondary);
+        elements_text_box(
+            canvas,
+            1,
+            FRAME_HEIGHT + 1,
+            118,
+            (63 - FRAME_HEIGHT * 2),
+            AlignLeft,
+            AlignTop,
+            furi_string_get_cstr(model->file_path),
+            false);
+    } else if(furi_string_empty(model->file_path)) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 1, FRAME_HEIGHT * 2 - 2, "Button not set");
+    } else {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 1, FRAME_HEIGHT * 2 - 2, "ERR:");
+        canvas_set_font(canvas, FontSecondary);
+        switch(model->sub_state) {
+        case SubRemLoadSubStateErrorNoFile:
+            canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "File not found");
+            break;
+        case SubRemLoadSubStateErrorFreq:
+            canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "Bad frequency");
+            break;
+        case SubRemLoadSubStateErrorMod:
+            canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "Bad modulation");
+            break;
+        case SubRemLoadSubStateErrorProtocol:
+            canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "Unsupported protocol");
+            break;
+
+        default:
+            break;
+        }
+        elements_text_box(
+            canvas,
+            1,
+            FRAME_HEIGHT * 2,
+            118,
+            30,
+            AlignLeft,
+            AlignTop,
+            furi_string_get_cstr(model->file_path),
+            false);
+    }
+}
+
+bool subrem_view_edit_menu_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    SubRemViewEditMenu* subrem_view_edit_menu = context;
+
+    if((event->key == InputKeyBack || event->key == InputKeyLeft) &&
+       event->type == InputTypeShort) {
+        subrem_view_edit_menu->callback(
+            SubRemCustomEventViewEditMenuBack, subrem_view_edit_menu->context);
+        return true;
+    } else if(event->key == InputKeyUp && event->type == InputTypeShort) {
+        with_view_model(
+            subrem_view_edit_menu->view,
+            SubRemViewEditMenuModel * model,
+            {
+                if(model->chosen > 0) {
+                    model->chosen -= 1;
+                };
+            },
+            true);
+        subrem_view_edit_menu->callback(
+            SubRemCustomEventViewEditMenuUP, subrem_view_edit_menu->context);
+        return true;
+    } else if(event->key == InputKeyDown && event->type == InputTypeShort) {
+        with_view_model(
+            subrem_view_edit_menu->view,
+            SubRemViewEditMenuModel * model,
+            {
+                if(model->chosen < 4) {
+                    model->chosen += 1;
+                };
+            },
+            true);
+        subrem_view_edit_menu->callback(
+            SubRemCustomEventViewEditMenuDOWN, subrem_view_edit_menu->context);
+        return true;
+    } else if(event->key == InputKeyOk && event->type == InputTypeShort) {
+        subrem_view_edit_menu->callback(
+            SubRemCustomEventViewEditMenuEdit, subrem_view_edit_menu->context);
+        return true;
+    } else if(event->key == InputKeyRight && event->type == InputTypeShort) {
+        subrem_view_edit_menu->callback(
+            SubRemCustomEventViewEditMenuSave, subrem_view_edit_menu->context);
+        return true;
+    }
+
+    return true;
+}
+
+void subrem_view_edit_menu_enter(void* context) {
+    furi_assert(context);
+}
+
+void subrem_view_edit_menu_exit(void* context) {
+    furi_assert(context);
+}
+
+SubRemViewEditMenu* subrem_view_edit_menu_alloc(void) {
+    SubRemViewEditMenu* subrem_view_edit_menu = malloc(sizeof(SubRemViewEditMenu));
+
+    // View allocation and configuration
+    subrem_view_edit_menu->view = view_alloc();
+    view_allocate_model(
+        subrem_view_edit_menu->view, ViewModelTypeLocking, sizeof(SubRemViewEditMenuModel));
+    view_set_context(subrem_view_edit_menu->view, subrem_view_edit_menu);
+    view_set_draw_callback(
+        subrem_view_edit_menu->view, (ViewDrawCallback)subrem_view_edit_menu_draw);
+    view_set_input_callback(subrem_view_edit_menu->view, subrem_view_edit_menu_input);
+    view_set_enter_callback(subrem_view_edit_menu->view, subrem_view_edit_menu_enter);
+    view_set_exit_callback(subrem_view_edit_menu->view, subrem_view_edit_menu_exit);
+
+    with_view_model(
+        subrem_view_edit_menu->view,
+        SubRemViewEditMenuModel * model,
+        {
+            model->label = furi_string_alloc(); // furi_string_alloc_set_str("LABEL");
+            model->file_path = furi_string_alloc(); // furi_string_alloc_set_str("FILE_PATH");
+
+            model->chosen = 0;
+        },
+        true);
+    return subrem_view_edit_menu;
+}
+
+void subrem_view_edit_menu_free(SubRemViewEditMenu* subghz_edit_menu) {
+    furi_assert(subghz_edit_menu);
+
+    with_view_model(
+        subghz_edit_menu->view,
+        SubRemViewEditMenuModel * model,
+        {
+            furi_string_free(model->label);
+            furi_string_free(model->file_path);
+        },
+        true);
+    view_free(subghz_edit_menu->view);
+    free(subghz_edit_menu);
+}
+
+View* subrem_view_edit_menu_get_view(SubRemViewEditMenu* subrem_view_edit_menu) {
+    furi_assert(subrem_view_edit_menu);
+    return subrem_view_edit_menu->view;
+}

+ 29 - 0
subghz_remote/views/edit_menu.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/subrem_custom_event.h"
+#include "../helpers/subrem_presets.h"
+
+typedef struct SubRemViewEditMenu SubRemViewEditMenu;
+
+typedef void (*SubRemViewEditMenuCallback)(SubRemCustomEvent event, void* context);
+
+void subrem_view_edit_menu_set_callback(
+    SubRemViewEditMenu* subrem_view_edit_menu,
+    SubRemViewEditMenuCallback callback,
+    void* context);
+
+SubRemViewEditMenu* subrem_view_edit_menu_alloc(void);
+
+void subrem_view_edit_menu_free(SubRemViewEditMenu* subrem_view_edit_menu);
+
+View* subrem_view_edit_menu_get_view(SubRemViewEditMenu* subrem_view_edit_menu);
+
+void subrem_view_edit_menu_add_data_to_show(
+    SubRemViewEditMenu* subrem_view_edit_remote,
+    uint8_t index,
+    FuriString* label,
+    FuriString* path,
+    SubRemLoadSubState state);
+
+uint8_t subrem_view_edit_menu_get_index(SubRemViewEditMenu* subrem_view_edit_remote);

+ 323 - 0
subghz_remote/views/remote.c

@@ -0,0 +1,323 @@
+#include "remote.h"
+#include "../subghz_remote_app_i.h"
+
+#include <input/input.h>
+#include <gui/elements.h>
+
+#include <lib/toolbox/path.h>
+
+#define SUBREM_VIEW_REMOTE_MAX_LABEL_LENGTH 30
+#define SUBREM_VIEW_REMOTE_LEFT_OFFSET 10
+#define SUBREM_VIEW_REMOTE_RIGHT_OFFSET 0
+
+struct SubRemViewRemote {
+    View* view;
+    SubRemViewRemoteCallback callback;
+    void* context;
+};
+
+typedef struct {
+    char* labels[SubRemSubKeyNameMaxCount];
+
+    SubRemViewRemoteState state;
+
+    uint8_t pressed_btn;
+    bool is_external;
+} SubRemViewRemoteModel;
+
+void subrem_view_remote_set_callback(
+    SubRemViewRemote* subrem_view_remote,
+    SubRemViewRemoteCallback callback,
+    void* context) {
+    furi_assert(subrem_view_remote);
+
+    subrem_view_remote->callback = callback;
+    subrem_view_remote->context = context;
+}
+
+void subrem_view_remote_update_data_labels(
+    SubRemViewRemote* subrem_view_remote,
+    SubRemSubFilePreset** subs_presets) {
+    furi_assert(subrem_view_remote);
+    furi_assert(subs_presets);
+
+    FuriString* labels[SubRemSubKeyNameMaxCount];
+    SubRemSubFilePreset* sub_preset;
+
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        sub_preset = subs_presets[i];
+        switch(sub_preset->load_state) {
+        case SubRemLoadSubStateOK:
+            if(!furi_string_empty(sub_preset->label)) {
+                labels[i] = furi_string_alloc_set(sub_preset->label);
+            } else if(!furi_string_empty(sub_preset->file_path)) {
+                labels[i] = furi_string_alloc();
+                path_extract_filename(sub_preset->file_path, labels[i], true);
+            } else {
+                labels[i] = furi_string_alloc_set("Empty Label");
+            }
+            break;
+
+        case SubRemLoadSubStateErrorNoFile:
+            labels[i] = furi_string_alloc_set("[X] Can't open file");
+            break;
+
+        case SubRemLoadSubStateErrorFreq:
+        case SubRemLoadSubStateErrorMod:
+        case SubRemLoadSubStateErrorProtocol:
+            labels[i] = furi_string_alloc_set("[X] Error in .sub file");
+            break;
+
+        default:
+            labels[i] = furi_string_alloc_set("");
+            break;
+        }
+    }
+
+    with_view_model(
+        subrem_view_remote->view,
+        SubRemViewRemoteModel * model,
+        {
+            for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+                strncpy(
+                    model->labels[i],
+                    furi_string_get_cstr(labels[i]),
+                    SUBREM_VIEW_REMOTE_MAX_LABEL_LENGTH);
+            }
+        },
+        true);
+
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        furi_string_free(labels[i]);
+    }
+}
+
+void subrem_view_remote_set_state(
+    SubRemViewRemote* subrem_view_remote,
+    SubRemViewRemoteState state,
+    uint8_t presed_btn) {
+    furi_assert(subrem_view_remote);
+    with_view_model(
+        subrem_view_remote->view,
+        SubRemViewRemoteModel * model,
+        {
+            model->state = state;
+            model->pressed_btn = presed_btn;
+        },
+        true);
+}
+
+void subrem_view_remote_set_radio(SubRemViewRemote* subrem_view_remote, bool external) {
+    furi_assert(subrem_view_remote);
+    with_view_model(
+        subrem_view_remote->view,
+        SubRemViewRemoteModel * model,
+        { model->is_external = external; },
+        true);
+}
+
+void subrem_view_remote_draw(Canvas* canvas, SubRemViewRemoteModel* model) {
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Statusbar
+    canvas_draw_icon(canvas, 0, 0, &I_status_bar);
+    if(model->state == SubRemViewRemoteStateOFF) {
+        canvas_invert_color(canvas);
+        canvas_draw_rbox(canvas, 12, 0, 52 - 12, 13, 2);
+        canvas_invert_color(canvas);
+        canvas_draw_rframe(canvas, 12, 0, 52 - 12, 13, 2);
+        canvas_draw_str_aligned(canvas, 32, 3, AlignCenter, AlignTop, "Preview");
+    } else {
+        canvas_draw_icon(
+            canvas,
+            0,
+            2,
+            (model->is_external) ? &I_External_antenna_20x12 : &I_Internal_antenna_20x12);
+        canvas_draw_icon(canvas, 50, 0, &I_Status_cube_14x14);
+        if(model->state == SubRemViewRemoteStateSending) {
+            canvas_draw_icon_ex(canvas, 52, 3, &I_Pin_arrow_up_7x9, IconRotation90);
+        }
+    }
+
+    //Icons for Labels
+    const uint8_t list_y = 14;
+    canvas_draw_icon(canvas, 1, list_y + 5, &I_ButtonUp_7x4);
+    canvas_draw_icon(canvas, 1, list_y + 15, &I_ButtonDown_7x4);
+    canvas_draw_icon(canvas, 2, list_y + 23, &I_ButtonLeft_4x7);
+    canvas_draw_icon(canvas, 2, list_y + 33, &I_ButtonRight_4x7);
+    canvas_draw_icon(canvas, 0, list_y + 42, &I_Ok_btn_9x9);
+
+    //Labels
+    canvas_set_font(canvas, FontSecondary);
+    uint8_t y = list_y;
+    for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+        elements_text_box(
+            canvas,
+            SUBREM_VIEW_REMOTE_LEFT_OFFSET,
+            y + 2,
+            64 - SUBREM_VIEW_REMOTE_LEFT_OFFSET - SUBREM_VIEW_REMOTE_RIGHT_OFFSET,
+            12,
+            AlignLeft,
+            AlignBottom,
+            model->labels[i],
+            false);
+        y += 10;
+    }
+
+    if(model->state != SubRemViewRemoteStateOFF) {
+        // D-pad 59x62
+        const uint8_t d_pad_x = 3;
+        const uint8_t d_pad_y = 66;
+
+        canvas_draw_icon(canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 0 * (20 + 1), &I_up);
+
+        canvas_draw_icon(canvas, d_pad_x + 0 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_left);
+        canvas_draw_icon(canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_ok);
+        canvas_draw_icon(canvas, d_pad_x + 2 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_right);
+
+        canvas_draw_icon(canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 2 * (20 + 1), &I_down);
+        if(model->state == SubRemViewRemoteStateSending) {
+            switch(model->pressed_btn) {
+            case SubRemSubKeyNameUp:
+                canvas_draw_icon(
+                    canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 0 * (20 + 1), &I_up_hover);
+                break;
+            case SubRemSubKeyNameDown:
+                canvas_draw_icon(
+                    canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 2 * (20 + 1), &I_down_hover);
+                break;
+            case SubRemSubKeyNameLeft:
+                canvas_draw_icon(
+                    canvas, d_pad_x + 0 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_left_hover);
+                break;
+            case SubRemSubKeyNameRight:
+                canvas_draw_icon(
+                    canvas, d_pad_x + 2 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_right_hover);
+                break;
+            case SubRemSubKeyNameOk:
+                canvas_draw_icon(
+                    canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_ok_hover);
+                break;
+            default:
+                break;
+            }
+        }
+    } else {
+        canvas_draw_icon(canvas, 2, 128 - 11, &I_ButtonLeft_4x7);
+        canvas_draw_str_aligned(canvas, 8, 128 - 4, AlignLeft, AlignBottom, "Back");
+
+        canvas_draw_icon(canvas, 58, 128 - 11, &I_ButtonRight_4x7);
+        canvas_draw_str_aligned(canvas, 56, 128 - 4, AlignRight, AlignBottom, "Save");
+    }
+}
+
+bool subrem_view_remote_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    SubRemViewRemote* subrem_view_remote = context;
+
+    if(event->key == InputKeyBack && event->type == InputTypeLong) {
+        subrem_view_remote->callback(SubRemCustomEventViewRemoteBack, subrem_view_remote->context);
+        return true;
+    } else if(event->key == InputKeyBack && event->type == InputTypeShort) {
+        with_view_model(
+            subrem_view_remote->view,
+            SubRemViewRemoteModel * model,
+            { model->pressed_btn = 0; },
+            true);
+        subrem_view_remote->callback(
+            SubRemCustomEventViewRemoteForcedStop, subrem_view_remote->context);
+        return true;
+    } else if(event->key == InputKeyBack) {
+        return true;
+    }
+    // BACK button processing end
+
+    if(event->key == InputKeyUp && event->type == InputTypePress) {
+        subrem_view_remote->callback(
+            SubRemCustomEventViewRemoteStartUP, subrem_view_remote->context);
+        return true;
+    } else if(event->key == InputKeyDown && event->type == InputTypePress) {
+        subrem_view_remote->callback(
+            SubRemCustomEventViewRemoteStartDOWN, subrem_view_remote->context);
+        return true;
+    } else if(event->key == InputKeyLeft && event->type == InputTypePress) {
+        subrem_view_remote->callback(
+            SubRemCustomEventViewRemoteStartLEFT, subrem_view_remote->context);
+        return true;
+    } else if(event->key == InputKeyRight && event->type == InputTypePress) {
+        subrem_view_remote->callback(
+            SubRemCustomEventViewRemoteStartRIGHT, subrem_view_remote->context);
+        return true;
+    } else if(event->key == InputKeyOk && event->type == InputTypePress) {
+        subrem_view_remote->callback(
+            SubRemCustomEventViewRemoteStartOK, subrem_view_remote->context);
+        return true;
+    } else if(event->type == InputTypeRelease) {
+        subrem_view_remote->callback(SubRemCustomEventViewRemoteStop, subrem_view_remote->context);
+        return true;
+    }
+
+    return true;
+}
+
+void subrem_view_remote_enter(void* context) {
+    furi_assert(context);
+}
+
+void subrem_view_remote_exit(void* context) {
+    furi_assert(context);
+}
+
+SubRemViewRemote* subrem_view_remote_alloc(void) {
+    SubRemViewRemote* subrem_view_remote = malloc(sizeof(SubRemViewRemote));
+
+    // View allocation and configuration
+    subrem_view_remote->view = view_alloc();
+    view_allocate_model(
+        subrem_view_remote->view, ViewModelTypeLocking, sizeof(SubRemViewRemoteModel));
+    view_set_context(subrem_view_remote->view, subrem_view_remote);
+    view_set_orientation(subrem_view_remote->view, ViewOrientationVertical);
+    view_set_draw_callback(subrem_view_remote->view, (ViewDrawCallback)subrem_view_remote_draw);
+    view_set_input_callback(subrem_view_remote->view, subrem_view_remote_input);
+    view_set_enter_callback(subrem_view_remote->view, subrem_view_remote_enter);
+    view_set_exit_callback(subrem_view_remote->view, subrem_view_remote_exit);
+
+    with_view_model(
+        subrem_view_remote->view,
+        SubRemViewRemoteModel * model,
+        {
+            model->state = SubRemViewRemoteStateIdle;
+
+            for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+                model->labels[i] = malloc(sizeof(char) * SUBREM_VIEW_REMOTE_MAX_LABEL_LENGTH + 1);
+                strcpy(model->labels[i], "");
+            }
+
+            model->pressed_btn = 0;
+            model->is_external = false;
+        },
+        true);
+    return subrem_view_remote;
+}
+
+void subrem_view_remote_free(SubRemViewRemote* subghz_remote) {
+    furi_assert(subghz_remote);
+
+    with_view_model(
+        subghz_remote->view,
+        SubRemViewRemoteModel * model,
+        {
+            for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) {
+                free(model->labels[i]);
+            }
+        },
+        true);
+    view_free(subghz_remote->view);
+    free(subghz_remote);
+}
+
+View* subrem_view_remote_get_view(SubRemViewRemote* subrem_view_remote) {
+    furi_assert(subrem_view_remote);
+    return subrem_view_remote->view;
+}

+ 38 - 0
subghz_remote/views/remote.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/subrem_custom_event.h"
+#include "../helpers/subrem_presets.h"
+
+typedef enum {
+    SubRemViewRemoteStateIdle,
+    SubRemViewRemoteStateLoading,
+    SubRemViewRemoteStateSending,
+    SubRemViewRemoteStateOFF,
+} SubRemViewRemoteState;
+
+typedef struct SubRemViewRemote SubRemViewRemote;
+
+typedef void (*SubRemViewRemoteCallback)(SubRemCustomEvent event, void* context);
+
+void subrem_view_remote_set_callback(
+    SubRemViewRemote* subrem_view_remote,
+    SubRemViewRemoteCallback callback,
+    void* context);
+
+SubRemViewRemote* subrem_view_remote_alloc(void);
+
+void subrem_view_remote_free(SubRemViewRemote* subrem_view_remote);
+
+View* subrem_view_remote_get_view(SubRemViewRemote* subrem_view_remote);
+
+void subrem_view_remote_update_data_labels(
+    SubRemViewRemote* subrem_view_remote,
+    SubRemSubFilePreset** subs_presets);
+
+void subrem_view_remote_set_state(
+    SubRemViewRemote* subrem_view_remote,
+    SubRemViewRemoteState state,
+    uint8_t presed_btn);
+
+void subrem_view_remote_set_radio(SubRemViewRemote* subrem_view_remote, bool external);