Преглед изворни кода

fix(subghz): dynamic subghz signals update file on tx (#19)

Dynamic SubGhz signals were not re-saving with the incremented counter/values after transmission
Added SubGhz Duration setting to control non-RAW signal transmission duration
Added support to change duration in Quac Playlist files
Removed SubGhz Repeat setting as it's no longer needed
Removed SubGhz External Antenna setting as it now prefers to use the ext antenna if available

---------

Co-authored-by: WillyJL <49810075+WillyJL@users.noreply.github.com>
rdefeo пре 7 месеци
родитељ
комит
703f3e5c68

+ 9 - 0
CHANGELOG.md

@@ -1,3 +1,12 @@
+## 0.9.0
+
+- Complete refactor of SubGhz code
+- Supports dynamic SubGhz signals with rolling codes
+- Auto-detect External SubGhz Antenna and use/prefer if available
+- Removed SugGhz External Antenna setting due to new auto-detect feature
+- Removed SubGhz Repeat setting, no longer needed (was non-standard)
+- Added SubGhz Duration setting for non-RAW signals
+
 ## 0.8
 
 - Added Import Link Here, which creates Quac Link files

+ 4 - 2
README.md

@@ -44,6 +44,8 @@ The app does not provide any recording functionality - you must use the existing
 
 The signal files are played back as recorded. During playback/transmit, the LED light will flash blue until the action is complete. For RFID, NFC, and iButton signals, they are continuously played back for their defined durations: RFID - 2.5 seconds, NFC - 1 second, iButton - 1 second. These defaults can be changed in [application Settings](README.md#application-settings).
 
+SubGhz signals that are dynamic (i.e. have rolling codes / counters) will be re-saved on playback, ensuring the persistence of the new counter values. If an External SubGhz antenna is available, it will be used.
+
 ## Signal Organization
 
 The key to organizing your Quac! interface is to organize your `/ext/apps_data/quac` folder structure. The UI is derived directly from the filesystem structure. Every individual file/signal is given a label on screen. And every folder/directory is a logical group of more files/folders. Selecting a group in the UI will show you the contents of that folder. There is no limit on the number of actions or folders - nest as deep as you want!
@@ -88,7 +90,7 @@ You can chain multiple signal playback actions together by creating a playlist.
 * Comments: lines that start with a `#` are ignored
 * `pause <ms>` on a line will pause the playback by the specified millisecond duration
 * Signal file names can be absolute (full path) or relative to the current directory
-* RFID and NFC files can have an optional duration specified. Simply add a space after the signal's file name, followed by a millisecond duration. This duration will override the Quac! Settings value, just for this one signal.
+* SubGhz, RFID, and NFC files can have an optional duration specified. Simply add a space after the signal's file name, followed by a millisecond duration. This duration will override the Quac! Settings value, just for this one signal.
 
 Errors found in the playlist will halt playback and vibrate the Flipper. Blank lines are ignored.
 
@@ -124,10 +126,10 @@ The settings menu will appear as the last item when you are viewing the "root" d
 * Layout: Switch between Horizontal and Vertical layout
 * Show Icons: Toggles display of all icons
 * Show Headers: Toggles display of header/folder text at the top, giving you room for one more item on screen!
+* SubGhz Duration: Changes the length of time a non-RAW SubGhz signal is trasmitted. This allows for repeated transmissions during the interval. Within playlists, this can be overridden per `.sub` file.
 * RFID Duration: Changes the length of time a RFID signal is transmitted. Within playlists, this can be overridden per `.rfid` file.
 * NFC Duration: Changes the length of time a NFC signal is transmitted. Within playlists, this can be overridden per `.nfc` file.
 * iButton Duration: Changes the length of time a iButton signal is transmitted. Within playlists, this can be overridden per `.ibtn` file.
-* SubGhz Ext Ant: Whether to try using the external antenna for sub-ghz signals. If this is "Enabled" but no external antenna is attached, or the external antenna can't be accessed, Quac! will fall back to using the internal antenna.
 * IR Ext Ant: Whether to use the external device for IR signals. If enabled, but no external IR device is attached to TX, then the internal IR device will be used.
 * Show Hidden: Will display files and folders that start with a period (`.`)
 * About: Application info and version

+ 3 - 3
README_flipperlab.md

@@ -10,7 +10,7 @@ The app does not provide any recording functionality - you can use the existing
 
 ## Features
 
-* Playback of rfid, sub-ghz, IR, NFC, and iButton signals
+* Playback of sub-ghz, rfid, IR, NFC, and iButton signals
 * Easy navigation
 * Flexible signal organization
 * In-app file management
@@ -20,7 +20,7 @@ The app does not provide any recording functionality - you can use the existing
 
 ## Signal playback
 
-The signal files are played back as recorded. During playback/transmit, the LED light will flash blue until the action is complete. For RFID, NFC, anbd iButton signals, they are continuously played back for the durations specified in the Settings.
+The signal files are played back as recorded. During playback/transmit, the LED light will flash blue until the action is complete. For SubGhz (non-RAW), RFID, NFC, and iButton signals, they are continuously played back for the durations specified in the Settings.
 
 ## Signal Organization
 
@@ -43,10 +43,10 @@ The settings menu will appear as the last item when you are viewing the "root" d
 * Layout: Switch between Horizontal and Vertical layout
 * Show Icons: Toggles display of all icons
 * Show Headers: Toggles display of header/folder text at the top, giving you room for one more item on screen!
+* SubGhz Duration: Changes the length of time a non-RAW SubGhz signal is transmitted. Can be overridden, per SubGhz file in a Playlist
 * RFID Duration: Changes the length of time a RFID signal is transmitted. Can be overridden, per RFID file in a Playlist
 * NFC Duration: Changes the length of time a NFC signal is transmitted. Can be overridden, per NFC file in a Playlist
 * iButton Duration: Changes the length of time a iButton signal is transmitted. Can be overridden, per iButton file in a Playlist
-* SubGhz Ext Ant: Enables / Disables use of external sub-ghz antenna
 * IR Ext Ant: Enables / Disables use of external IR antenna
 * Show Hidden: Toggles display of files/folders that start with a period.
 

+ 0 - 2
actions/action.c

@@ -3,8 +3,6 @@
 #include "item.h"
 #include "action_i.h"
 
-#define MAX_FILE_LEN (size_t)256
-
 void action_ql_resolve(
     void* context,
     const FuriString* action_path,

+ 11 - 1
actions/action_qpl.c

@@ -31,6 +31,7 @@ void action_qpl_tx(void* context, const FuriString* action_path, FuriString* err
     App* app = context;
 
     // Save the current durations, in case the are changed during playback
+    uint32_t orig_subghz_duration = app->settings.subghz_duration;
     uint32_t orig_rfid_duration = app->settings.rfid_duration;
     uint32_t orig_nfc_duration = app->settings.nfc_duration;
     uint32_t orig_ibutton_duration = app->settings.ibutton_duration;
@@ -97,7 +98,14 @@ void action_qpl_tx(void* context, const FuriString* action_path, FuriString* err
 
                 // FURI_LOG_I(TAG, " - Found extension of %s", ext);
 
-                if(!strcmp(ext, ".rfid")) {
+                if(!strcmp(ext, ".sub")) {
+                    uint32_t subghz_duration = 0;
+                    // FURI_LOG_I(TAG, "SubGhz file with duration");
+                    if(sscanf(furi_string_get_cstr(buffer), "%lu", &subghz_duration) == 1) {
+                        FURI_LOG_I(TAG, "SubGhz duration = %lu", subghz_duration);
+                        app->settings.subghz_duration = subghz_duration;
+                    }
+                } else if(!strcmp(ext, ".rfid")) {
                     uint32_t rfid_duration = 0;
                     // FURI_LOG_I(TAG, "RFID file with duration");
                     if(sscanf(furi_string_get_cstr(buffer), "%lu", &rfid_duration) == 1) {
@@ -142,6 +150,8 @@ void action_qpl_tx(void* context, const FuriString* action_path, FuriString* err
             path_extract_extension(buffer, ext, MAX_EXT_LEN);
             if(!strcmp(ext, ".sub")) {
                 action_subghz_tx(context, buffer, error);
+                // Reset our default duration back - in case it was changed during playback
+                app->settings.subghz_duration = orig_subghz_duration;
             } else if(!strcmp(ext, ".rfid")) {
                 action_rfid_tx(context, buffer, error);
                 // Reset our default duration back - in case it was changed during playback

+ 110 - 213
actions/action_subghz.c

@@ -1,120 +1,82 @@
 // Methods for Sub-GHz transmission
 
-// subghz
-#include <lib/subghz/transmitter.h>
-#include <lib/subghz/devices/devices.h>
-#include <lib/subghz/devices/cc1101_configs.h>
-#include <lib/subghz/protocols/raw.h>
-#include <lib/subghz/subghz_protocol_registry.h>
+#include <flipper_format/flipper_format_i.h>
 
-#include <flipper_format/flipper_format.h>
+#include "helpers/subghz_txrx.h"
 
 #include "action_i.h"
 #include "quac.h"
 
-#define SUBGHZ_DEVICE_CC1101_EXT_NAME "cc1101_ext"
-#define SUBGHZ_DEVICE_CC1101_INT_NAME "cc1101_int"
-
-static FuriHalSubGhzPreset action_subghz_get_preset_name(const char* preset_name) {
-    FuriHalSubGhzPreset preset = FuriHalSubGhzPresetIDLE;
-    if(!strcmp(preset_name, "FuriHalSubGhzPresetOok270Async")) {
-        preset = FuriHalSubGhzPresetOok270Async;
-    } else if(!strcmp(preset_name, "FuriHalSubGhzPresetOok650Async")) {
-        preset = FuriHalSubGhzPresetOok650Async;
-    } else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev238Async")) {
-        preset = FuriHalSubGhzPreset2FSKDev238Async;
-    } else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev476Async")) {
-        preset = FuriHalSubGhzPreset2FSKDev476Async;
-    } else if(!strcmp(preset_name, "FuriHalSubGhzPresetCustom")) {
-        preset = FuriHalSubGhzPresetCustom;
-    } else {
-        FURI_LOG_E(TAG, "SUBGHZ: Unknown preset!");
-    }
-    return preset;
-}
+typedef struct SubGhzNeedSaveContext {
+    App* app;
+    SubGhzTxRx* txrx;
+    const FuriString* file_path;
+} SubGhzNeedSaveContext;
 
-static const SubGhzDevice* action_subghz_get_device(uint32_t* device_ind) {
-    const SubGhzDevice* device = NULL;
-    switch(*device_ind) {
-    case 1: {
-        // Power on the external antenna
-        uint8_t attempts = 5;
-        while(--attempts > 0) {
-            if(furi_hal_power_enable_otg()) break;
-        }
-        if(attempts == 0) {
-            if(furi_hal_power_get_usb_voltage() < 4.5f) {
-                FURI_LOG_E(
-                    TAG,
-                    "Error power otg enable. BQ2589 check otg fault = %d",
-                    furi_hal_power_check_otg_fault() ? 1 : 0);
-            }
-        }
-        device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
-        break;
-    }
-    default:
-        device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
-        break;
-    }
-    if(!subghz_devices_is_connect(device)) {
-        // Power off
-        if(furi_hal_power_is_otg_enabled()) {
-            furi_hal_power_disable_otg();
+void action_subghz_need_save_callback(void* context) {
+    FURI_LOG_I(TAG, "Saving udpated subghz signal");
+    SubGhzNeedSaveContext* savectx = (SubGhzNeedSaveContext*)context;
+    FlipperFormat* ff = subghz_txrx_get_fff_data(savectx->txrx);
+
+    Stream* ff_stream = flipper_format_get_raw_stream(ff);
+    flipper_format_delete_key(ff, "Repeat");
+    flipper_format_delete_key(ff, "Manufacture");
+
+    do {
+        if(!storage_simply_remove(
+               savectx->app->storage, furi_string_get_cstr(savectx->file_path))) {
+            FURI_LOG_E(TAG, "Failed to delete subghz file before re-save");
+            break;
         }
-        if(*device_ind == 1) {
-            FURI_LOG_W(TAG, "Can't connect to External antenna, using Internal");
+        stream_seek(ff_stream, 0, StreamOffsetFromStart);
+        stream_save_to_file(
+            ff_stream,
+            savectx->app->storage,
+            furi_string_get_cstr(savectx->file_path),
+            FSOM_CREATE_ALWAYS);
+        if(storage_common_stat(
+               savectx->app->storage, furi_string_get_cstr(savectx->file_path), NULL) != FSE_OK) {
+            FURI_LOG_E(TAG, "Error verifying new subghz file after re-save");
+            break;
         }
-        device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
-        *device_ind = 0;
-    }
-    return device;
+    } while(0);
+}
+
+static void action_subghz_raw_end_callback(void* context) {
+    FURI_LOG_I(TAG, "Stopping TX on RAW");
+    furi_assert(context);
+    FuriThread* thread = context;
+
+    furi_thread_flags_set(furi_thread_get_id(thread), 0);
 }
 
-// Lifted from flipperzero-firmware/applications/main/subghz/subghz_cli.c
 void action_subghz_tx(void* context, const FuriString* action_path, FuriString* error) {
     App* app = context;
     const char* file_name = furi_string_get_cstr(action_path);
-    uint32_t repeat = app->settings.subghz_repeat; // Defaults to 10 in the CLI
-    uint32_t device_ind = app->settings.subghz_use_ext_antenna ? 1 : 0;
 
     FlipperFormat* fff_data_file = flipper_format_file_alloc(app->storage);
-    FlipperFormat* fff_data_raw = flipper_format_string_alloc();
+
+    SubGhzTxRx* txrx = subghz_txrx_alloc();
+
+    SubGhzNeedSaveContext save_context = {app, txrx, action_path};
+    subghz_txrx_set_need_save_callback(txrx, action_subghz_need_save_callback, &save_context);
+
+    Stream* fff_data_stream = flipper_format_get_raw_stream(subghz_txrx_get_fff_data(txrx));
+    stream_clean(fff_data_stream);
+
+    FuriString* preset_name = furi_string_alloc();
+    FuriString* protocol_name = furi_string_alloc();
+    bool is_raw = false;
+
     FuriString* temp_str;
     temp_str = furi_string_alloc();
     uint32_t temp_data32;
-    bool check_file = false;
-    const SubGhzDevice* device = NULL;
 
     uint32_t frequency = 0;
-    SubGhzTransmitter* transmitter = NULL;
 
     FURI_LOG_I(TAG, "SUBGHZ: Action starting...");
 
-    subghz_devices_init();
-    SubGhzEnvironment* environment = subghz_environment_alloc();
-    if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME)) {
-        FURI_LOG_W(TAG, "Load_keystore keeloq_mfcodes - failed to load");
-    }
-    if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME)) {
-        FURI_LOG_W(TAG, "Load_keystore keeloq_mfcodes_user - failed to load");
-    }
-    subghz_environment_set_came_atomo_rainbow_table_file_name(
-        environment, SUBGHZ_CAME_ATOMO_DIR_NAME);
-    subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
-        environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
-    subghz_environment_set_nice_flor_s_rainbow_table_file_name(
-        environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
-    subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry);
-
     do {
-        device = action_subghz_get_device(&device_ind);
-        if(device == NULL) {
-            FURI_LOG_E(TAG, "Error device not found");
-            ACTION_SET_ERROR("SUBGHZ: Device not found");
-            break;
-        }
-
         if(!flipper_format_file_open_existing(fff_data_file, file_name)) {
             FURI_LOG_E(TAG, "Error opening %s", file_name);
             ACTION_SET_ERROR("SUBGHZ: Error opening %s", file_name);
@@ -136,14 +98,12 @@ void action_subghz_tx(void* context, const FuriString* action_path, FuriString*
             break;
         }
 
+        SubGhzSetting* setting = subghz_txrx_get_setting(txrx);
         if(!flipper_format_read_uint32(fff_data_file, "Frequency", &frequency, 1)) {
-            FURI_LOG_E(TAG, "Missing Frequency");
-            ACTION_SET_ERROR("SUBGHZ: Missing frequency");
-            break;
-        }
-
-        if(!subghz_devices_is_frequency_valid(device, frequency)) {
-            FURI_LOG_E(TAG, "Frequency not supported");
+            FURI_LOG_W(TAG, "Missing Frequency. Setting default frequency");
+            frequency = subghz_setting_get_default_frequency(setting);
+        } else if(!subghz_txrx_radio_device_is_frequecy_valid(txrx, frequency)) {
+            FURI_LOG_E(TAG, "Frequency not supported on the chosen radio module");
             ACTION_SET_ERROR("SUBGHZ: Frequency not supported");
             break;
         }
@@ -154,148 +114,85 @@ void action_subghz_tx(void* context, const FuriString* action_path, FuriString*
             break;
         }
 
-        FuriHalSubGhzPreset preset = action_subghz_get_preset_name(furi_string_get_cstr(temp_str));
-        if(preset == FuriHalSubGhzPresetIDLE) {
+        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), "")) {
+            FURI_LOG_E(TAG, "Unknown preset");
             ACTION_SET_ERROR("SUBGHZ: Unknown preset");
             break;
         }
 
-        subghz_devices_begin(device);
-        subghz_devices_reset(device);
-        subghz_devices_idle(device);
-
-        if(preset == FuriHalSubGhzPresetCustom) {
-            uint8_t* custom_preset_data;
-            uint32_t custom_preset_data_size;
-            if(!flipper_format_get_value_count(fff_data_file, "Custom_preset_data", &temp_data32))
-                break;
-            if(!temp_data32 || (temp_data32 % 2)) {
-                FURI_LOG_E(TAG, "Custom_preset_data size error");
-                ACTION_SET_ERROR("SUBGHZ: Custom_preset_data size error");
+        if(!strcmp(furi_string_get_cstr(temp_str), "CUSTOM")) {
+            subghz_setting_delete_custom_preset(setting, furi_string_get_cstr(temp_str));
+            if(!subghz_setting_load_custom_preset(
+                   setting, furi_string_get_cstr(temp_str), fff_data_file)) {
+                FURI_LOG_E(TAG, "Missing Custom preset");
+                ACTION_SET_ERROR("SUBGHZ: Missing Custom preset");
                 break;
             }
-            custom_preset_data_size = sizeof(uint8_t) * temp_data32;
-            custom_preset_data = malloc(custom_preset_data_size);
-            if(!flipper_format_read_hex(
-                   fff_data_file,
-                   "Custom_preset_data",
-                   custom_preset_data,
-                   custom_preset_data_size)) {
-                FURI_LOG_E(TAG, "Custom_preset_data read error");
-                ACTION_SET_ERROR("SUBGHZ: Custom_preset_data read error");
-                break;
-            }
-            subghz_devices_load_preset(device, preset, custom_preset_data);
-            free(custom_preset_data);
-        } else {
-            subghz_devices_load_preset(device, preset, NULL);
         }
-
-        subghz_devices_set_frequency(device, frequency);
+        furi_string_set(preset_name, temp_str);
+        size_t preset_index =
+            subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(preset_name));
+        subghz_txrx_set_preset(
+            txrx,
+            furi_string_get_cstr(preset_name),
+            frequency,
+            subghz_setting_get_preset_data(setting, preset_index),
+            subghz_setting_get_preset_data_size(setting, preset_index));
 
         // Load Protocol
-        if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
+        if(!flipper_format_read_string(fff_data_file, "Protocol", protocol_name)) {
             FURI_LOG_E(TAG, "Missing protocol");
             ACTION_SET_ERROR("SUBGHZ: Missing protocol");
             break;
         }
 
-        SubGhzProtocolStatus status;
-        bool is_init_protocol = true;
-        if(furi_string_equal(temp_str, "RAW")) {
-            FURI_LOG_I(TAG, "Protocol = RAW");
+        FlipperFormat* fff_data = subghz_txrx_get_fff_data(txrx);
+        if(!strcmp(furi_string_get_cstr(protocol_name), "RAW")) {
             subghz_protocol_raw_gen_fff_data(
-                fff_data_raw, file_name, subghz_devices_get_name(device));
-            transmitter =
-                subghz_transmitter_alloc_init(environment, furi_string_get_cstr(temp_str));
-            if(transmitter == NULL) {
-                FURI_LOG_E(TAG, "Error transmitter");
-                is_init_protocol = false;
-            }
-
-            if(is_init_protocol) {
-                status = subghz_transmitter_deserialize(transmitter, fff_data_raw);
-                if(status != SubGhzProtocolStatusOk) {
-                    FURI_LOG_E(TAG, "Error deserialize protocol");
-                    is_init_protocol = false;
-                }
-            }
-        } else { // if not RAW protocol
-            FURI_LOG_I(TAG, "Protocol != RAW");
-            flipper_format_insert_or_update_uint32(fff_data_file, "Repeat", &repeat, 1);
-            transmitter =
-                subghz_transmitter_alloc_init(environment, furi_string_get_cstr(temp_str));
-            if(transmitter == NULL) {
-                FURI_LOG_E(TAG, "Error transmitter");
-                is_init_protocol = false;
-            }
-            if(is_init_protocol) {
-                status = subghz_transmitter_deserialize(transmitter, fff_data_file);
-                if(status != SubGhzProtocolStatusOk) {
-                    FURI_LOG_E(TAG, "Error deserialize protocol");
-                    ACTION_SET_ERROR("SUBGHZ: Protocol error");
-                    is_init_protocol = false;
-                }
-            }
+                fff_data, file_name, subghz_txrx_radio_device_get_name(txrx));
+            is_raw = true;
+        } else {
+            stream_copy_full(
+                flipper_format_get_raw_stream(fff_data_file),
+                flipper_format_get_raw_stream(fff_data));
         }
 
-        if(is_init_protocol) {
-            check_file = true;
-        } else {
-            subghz_devices_sleep(device);
-            subghz_devices_end(device);
-            if(transmitter != NULL) {
-                subghz_transmitter_free(transmitter);
+        if(subghz_txrx_load_decoder_by_name_protocol(txrx, furi_string_get_cstr(protocol_name))) {
+            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: %s", furi_string_get_cstr(protocol_name));
+            break;
         }
     } while(false);
 
+    flipper_format_file_close(fff_data_file);
     flipper_format_free(fff_data_file);
 
-    if(check_file) {
-        furi_hal_power_suppress_charge_enter();
-        subghz_devices_set_tx(device);
-        FURI_LOG_I(
-            TAG,
-            "Transmitting at %s. Frequency=%lu, Protocol=%s",
-            file_name,
-            frequency,
-            furi_string_get_cstr(temp_str));
-        do {
-            // FURI_LOG_I(TAG, "delaying 200ms");
-            furi_delay_ms(100); // needed? orig 200
-            if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) {
-                while(!subghz_devices_is_async_complete_tx(device)) {
-                    // || cli_cmd_interrupt_received
-                    furi_delay_ms(100); // orig 333
-                }
-                subghz_devices_stop_async_tx(device);
-            } else {
-                FURI_LOG_W(TAG, "Transmission on this frequency is restricted in your region");
-            }
-
-            if(furi_string_equal(temp_str, "RAW")) {
-                subghz_transmitter_stop(transmitter);
-                repeat--;
-                // FURI_LOG_I(TAG, "decrementing repeat: %lu", repeat);
-                if(repeat) subghz_transmitter_deserialize(transmitter, fff_data_raw);
-            }
-
-        } while(repeat && furi_string_equal(temp_str, "RAW"));
-
-        subghz_devices_sleep(device);
-        subghz_devices_end(device);
-        // power off
-        if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+    if(subghz_txrx_tx_start(txrx, subghz_txrx_get_fff_data(txrx)) != SubGhzTxRxStartTxStateOk) {
+        FURI_LOG_E(TAG, "Failed to start TX");
+    }
 
-        furi_hal_power_suppress_charge_exit();
-        subghz_transmitter_free(transmitter);
+    if(is_raw) {
+        subghz_txrx_set_raw_file_encoder_worker_callback_end(
+            txrx, action_subghz_raw_end_callback, furi_thread_get_current());
+        furi_thread_flags_wait(0, FuriFlagWaitAll, FuriWaitForever);
+    } else {
+        furi_delay_ms(app->settings.subghz_duration);
     }
 
     FURI_LOG_I(TAG, "SUBGHZ: Action complete.");
 
-    flipper_format_free(fff_data_raw);
+    // This will call need_save_callback, if necessary
+    subghz_txrx_stop(txrx);
+
+    subghz_txrx_free(txrx);
+    furi_string_free(preset_name);
+    furi_string_free(protocol_name);
     furi_string_free(temp_str);
-    subghz_devices_deinit();
-    subghz_environment_free(environment);
 }

+ 613 - 0
actions/helpers/subghz_txrx.c

@@ -0,0 +1,613 @@
+#include "subghz_txrx_i.h" // IWYU pragma: keep
+
+#include <lib/subghz/subghz_protocol_registry.h>
+#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+
+#define TAG "SubGhz"
+
+static void subghz_txrx_radio_device_power_on(SubGhzTxRx* instance) {
+    UNUSED(instance);
+    uint8_t attempts = 5;
+    while(--attempts > 0) {
+        if(furi_hal_power_enable_otg()) break;
+    }
+    if(attempts == 0) {
+        if(furi_hal_power_get_usb_voltage() < 4.5f) {
+            FURI_LOG_E(
+                TAG,
+                "Error power otg enable. BQ2589 check otg fault = %d",
+                furi_hal_power_check_otg_fault() ? 1 : 0);
+        }
+    }
+}
+
+static void subghz_txrx_radio_device_power_off(SubGhzTxRx* instance) {
+    UNUSED(instance);
+    if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+}
+
+SubGhzTxRx* subghz_txrx_alloc(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);
+
+    instance->worker = subghz_worker_alloc();
+    instance->fff_data = flipper_format_string_alloc();
+
+    instance->environment = subghz_environment_alloc();
+    instance->is_database_loaded =
+        subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_NAME);
+    subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_USER_NAME);
+    subghz_environment_set_came_atomo_rainbow_table_file_name(
+        instance->environment, SUBGHZ_CAME_ATOMO_DIR_NAME);
+    subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
+        instance->environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
+    subghz_environment_set_nice_flor_s_rainbow_table_file_name(
+        instance->environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
+    subghz_environment_set_protocol_registry(
+        instance->environment, (void*)&subghz_protocol_registry);
+    instance->receiver = subghz_receiver_alloc_init(instance->environment);
+
+    subghz_worker_set_overrun_callback(
+        instance->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
+    subghz_worker_set_pair_callback(
+        instance->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
+    subghz_worker_set_context(instance->worker, instance->receiver);
+
+    //set default device External
+    subghz_devices_init();
+    instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
+    instance->radio_device_type =
+        subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101);
+    FURI_LOG_I(
+        TAG,
+        "radio device type = %s",
+        instance->radio_device_type == SubGhzRadioDeviceTypeInternal ? "Internal" : "External");
+
+    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) {
+    furi_assert(instance);
+    SubGhzRadioPreset* preset = instance->preset;
+    if(frequency != NULL) {
+        furi_string_printf(
+            frequency,
+            "%03ld.%02ld",
+            preset->frequency / 1000000 % 1000,
+            preset->frequency / 10000 % 100);
+    }
+    if(modulation != NULL) {
+        furi_string_printf(modulation, "%.2s", furi_string_get_cstr(preset->name));
+    }
+}
+
+static void subghz_txrx_begin(SubGhzTxRx* instance, uint8_t* preset_data) {
+    furi_assert(instance);
+    subghz_devices_reset(instance->radio_device);
+    subghz_devices_idle(instance->radio_device);
+    subghz_devices_load_preset(instance->radio_device, FuriHalSubGhzPresetCustom, preset_data);
+    instance->txrx_state = SubGhzTxRxStateIDLE;
+}
+
+static uint32_t subghz_txrx_rx(SubGhzTxRx* instance, uint32_t frequency) {
+    furi_assert(instance);
+
+    furi_assert(
+        instance->txrx_state != SubGhzTxRxStateRx && instance->txrx_state != SubGhzTxRxStateSleep);
+
+    subghz_devices_idle(instance->radio_device);
+
+    uint32_t value = subghz_devices_set_frequency(instance->radio_device, frequency);
+    subghz_devices_flush_rx(instance->radio_device);
+    subghz_txrx_speaker_on(instance);
+
+    subghz_devices_start_async_rx(
+        instance->radio_device, subghz_worker_rx_callback, instance->worker);
+    subghz_worker_start(instance->worker);
+    instance->txrx_state = SubGhzTxRxStateRx;
+    return value;
+}
+
+static void subghz_txrx_idle(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->txrx_state != SubGhzTxRxStateSleep) {
+        subghz_devices_idle(instance->radio_device);
+        subghz_txrx_speaker_off(instance);
+        instance->txrx_state = SubGhzTxRxStateIDLE;
+    }
+}
+
+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);
+}
+
+FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->fff_data;
+}
+
+SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->setting;
+}
+
+void subghz_txrx_stop(SubGhzTxRx* instance) {
+    furi_assert(instance);
+
+    switch(instance->txrx_state) {
+    case SubGhzTxRxStateTx:
+        subghz_txrx_tx_stop(instance);
+        subghz_txrx_speaker_unmute(instance);
+        break;
+    case SubGhzTxRxStateRx:
+        subghz_txrx_rx_end(instance);
+        subghz_txrx_speaker_mute(instance);
+        break;
+
+    default:
+        break;
+    }
+}
+
+void subghz_txrx_hopper_update(SubGhzTxRx* instance) {
+    furi_assert(instance);
+
+    switch(instance->hopper_state) {
+    case SubGhzHopperStateOFF:
+    case SubGhzHopperStatePause:
+        return;
+    case SubGhzHopperStateRSSITimeOut:
+        if(instance->hopper_timeout != 0) {
+            instance->hopper_timeout--;
+            return;
+        }
+        break;
+    default:
+        break;
+    }
+    float rssi = -127.0f;
+    if(instance->hopper_state != SubGhzHopperStateRSSITimeOut) {
+        // See RSSI Calculation timings in CC1101 17.3 RSSI
+        rssi = subghz_devices_get_rssi(instance->radio_device);
+
+        // Stay if RSSI is high enough
+        if(rssi > -90.0f) {
+            instance->hopper_timeout = 10;
+            instance->hopper_state = SubGhzHopperStateRSSITimeOut;
+            return;
+        }
+    } else {
+        instance->hopper_state = SubGhzHopperStateRunnig;
+    }
+    // Select next frequency
+    if(instance->hopper_idx_frequency <
+       subghz_setting_get_hopper_frequency_count(instance->setting) - 1) {
+        instance->hopper_idx_frequency++;
+    } else {
+        instance->hopper_idx_frequency = 0;
+    }
+
+    if(instance->txrx_state == SubGhzTxRxStateRx) {
+        subghz_txrx_rx_end(instance);
+    };
+    if(instance->txrx_state == SubGhzTxRxStateIDLE) {
+        subghz_receiver_reset(instance->receiver);
+        instance->preset->frequency =
+            subghz_setting_get_hopper_frequency(instance->setting, instance->hopper_idx_frequency);
+        subghz_txrx_rx(instance, instance->preset->frequency);
+    }
+}
+
+SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->hopper_state;
+}
+
+void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state) {
+    furi_assert(instance);
+    instance->hopper_state = state;
+}
+
+void subghz_txrx_hopper_unpause(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->hopper_state == SubGhzHopperStatePause) {
+        instance->hopper_state = SubGhzHopperStateRunnig;
+    }
+}
+
+void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->hopper_state == SubGhzHopperStateRunnig) {
+        instance->hopper_state = SubGhzHopperStatePause;
+    }
+}
+
+void subghz_txrx_speaker_on(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_acquire(30)) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker);
+        } else {
+            instance->speaker_state = SubGhzSpeakerStateDisable;
+        }
+    }
+}
+
+void subghz_txrx_speaker_off(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state != SubGhzSpeakerStateDisable) {
+        if(furi_hal_speaker_is_mine()) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+            furi_hal_speaker_release();
+            if(instance->speaker_state == SubGhzSpeakerStateShutdown)
+                instance->speaker_state = SubGhzSpeakerStateDisable;
+        }
+    }
+}
+
+void subghz_txrx_speaker_mute(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
+        }
+    }
+}
+
+void subghz_txrx_speaker_unmute(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    if(instance->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker);
+        }
+    }
+}
+
+void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state) {
+    furi_assert(instance);
+    instance->speaker_state = state;
+}
+
+SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->speaker_state;
+}
+
+bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol) {
+    furi_assert(instance);
+    furi_assert(name_protocol);
+    bool res = false;
+    instance->decoder_result =
+        subghz_receiver_search_decoder_base_by_name(instance->receiver, name_protocol);
+    if(instance->decoder_result) {
+        res = true;
+    }
+    return res;
+}
+
+SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->decoder_result;
+}
+
+bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return (instance->decoder_result->protocol->flag & SubGhzProtocolFlag_Save) ==
+           SubGhzProtocolFlag_Save;
+}
+
+bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type) {
+    furi_assert(instance);
+    const SubGhzProtocol* protocol = instance->decoder_result->protocol;
+    if(check_type) {
+        return ((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) &&
+               protocol->encoder->deserialize && protocol->type == SubGhzProtocolTypeStatic;
+    }
+    return ((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) &&
+           protocol->encoder->deserialize;
+}
+
+void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter) {
+    furi_assert(instance);
+    subghz_receiver_set_filter(instance->receiver, filter);
+}
+
+void subghz_txrx_set_rx_calback(
+    SubGhzTxRx* instance,
+    SubGhzReceiverCallback callback,
+    void* context) {
+    subghz_receiver_set_rx_callback(instance->receiver, callback, context);
+}
+
+void subghz_txrx_set_raw_file_encoder_worker_callback_end(
+    SubGhzTxRx* instance,
+    SubGhzProtocolEncoderRAWCallbackEnd callback,
+    void* context) {
+    subghz_protocol_raw_file_encoder_worker_set_callback_end(
+        (SubGhzProtocolEncoderRAW*)subghz_transmitter_get_protocol_instance(instance->transmitter),
+        callback,
+        context);
+}
+
+bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name) {
+    furi_assert(instance);
+
+    bool is_connect = false;
+    bool is_otg_enabled = furi_hal_power_is_otg_enabled();
+
+    if(!is_otg_enabled) {
+        subghz_txrx_radio_device_power_on(instance);
+    }
+
+    const SubGhzDevice* device = subghz_devices_get_by_name(name);
+    if(device) {
+        is_connect = subghz_devices_is_connect(device);
+    }
+
+    if(!is_otg_enabled) {
+        subghz_txrx_radio_device_power_off(instance);
+    }
+    return is_connect;
+}
+
+SubGhzRadioDeviceType
+    subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type) {
+    furi_assert(instance);
+
+    if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
+       subghz_txrx_radio_device_is_external_connected(instance, SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
+        subghz_txrx_radio_device_power_on(instance);
+        instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
+        subghz_devices_begin(instance->radio_device);
+        instance->radio_device_type = SubGhzRadioDeviceTypeExternalCC1101;
+    } else {
+        subghz_txrx_radio_device_power_off(instance);
+        if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) {
+            subghz_devices_end(instance->radio_device);
+        }
+        instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+        instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
+    }
+
+    return instance->radio_device_type;
+}
+
+SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return instance->radio_device_type;
+}
+
+float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return subghz_devices_get_rssi(instance->radio_device);
+}
+
+const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance) {
+    furi_assert(instance);
+    return subghz_devices_get_name(instance->radio_device);
+}
+
+bool subghz_txrx_radio_device_is_frequecy_valid(SubGhzTxRx* instance, uint32_t frequency) {
+    furi_assert(instance);
+    return subghz_devices_is_frequency_valid(instance->radio_device, frequency);
+}

+ 336 - 0
actions/helpers/subghz_txrx.h

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

+ 29 - 0
actions/helpers/subghz_txrx_i.h

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

+ 91 - 0
actions/helpers/subghz_types.h

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

+ 1 - 1
application.fam

@@ -8,7 +8,7 @@ App(
     stack_size=2 * 1024,
     fap_category="Tools",
     # Optional values
-    fap_version="0.8.0",
+    fap_version="0.9.0",
     fap_icon="images/quac.png",  # 10x10 1-bit PNG
     fap_description="Quick Action remote control app",
     fap_author="Roberto De Feo",

+ 0 - 1
quac.c

@@ -5,7 +5,6 @@
 
 #include "item.h"
 #include "scenes/scenes.h"
-#include "scenes/scene_items.h"
 
 /* generated by fbt from .png files in images folder */
 #include <quac_icons.h>

+ 2 - 3
quac.h

@@ -17,7 +17,7 @@
 #include "item.h"
 
 #define QUAC_NAME    "Quac!"
-#define QUAC_VERSION "v0.8.0"
+#define QUAC_VERSION "v0.9.0"
 #define QUAC_ABOUT                                    \
     "Quick Action remote control\n" QUAC_VERSION "\n" \
     "github.com/rdefeo/quac"
@@ -58,11 +58,10 @@ typedef struct App {
         QuacAppLayout layout; // Defaults to Portrait
         bool show_icons; // Defaults to True
         bool show_headers; // Defaults to True
+        uint32_t subghz_duration; // Defaults to 1500 ms
         uint32_t rfid_duration; // Defaults to 2500 ms
         uint32_t nfc_duration; // Defaults to 1000 ms
         uint32_t ibutton_duration; // Defaults to 1000 ms
-        uint32_t subghz_repeat; // Defaults to 10, just like the CLI
-        bool subghz_use_ext_antenna; // Defaults to False
         bool ir_use_ext_module; // Defaults to False
         bool show_hidden; // Defaults to False
     } settings;

+ 12 - 24
quac_settings.c

@@ -10,11 +10,10 @@ void quac_set_default_settings(App* app) {
     app->settings.layout = QUAC_APP_LANDSCAPE;
     app->settings.show_icons = true;
     app->settings.show_headers = true;
+    app->settings.subghz_duration = 1500;
     app->settings.rfid_duration = 2500;
     app->settings.nfc_duration = 1000;
     app->settings.ibutton_duration = 1000;
-    app->settings.subghz_repeat = 10;
-    app->settings.subghz_use_ext_antenna = false;
     app->settings.ir_use_ext_module = false;
     app->settings.show_hidden = false;
 }
@@ -72,6 +71,12 @@ void quac_load_settings(App* app) {
             app->settings.show_headers = (temp_data32 == 1) ? true : false;
         }
 
+        if(!flipper_format_read_uint32(fff_settings, "SubGhz Duration", &temp_data32, 1)) {
+            FURI_LOG_W(TAG, "SETTINGS: Missing 'SubGhz Duration'");
+        } else {
+            app->settings.subghz_duration = temp_data32;
+        }
+
         if(!flipper_format_read_uint32(fff_settings, "RFID Duration", &temp_data32, 1)) {
             FURI_LOG_W(TAG, "SETTINGS: Missing 'RFID Duration'");
         } else {
@@ -90,18 +95,6 @@ void quac_load_settings(App* app) {
             app->settings.ibutton_duration = temp_data32;
         }
 
-        if(!flipper_format_read_uint32(fff_settings, "SubGHz Repeat", &temp_data32, 1)) {
-            FURI_LOG_W(TAG, "SETTINGS: Missing 'SubGHz Repeat'");
-        } else {
-            app->settings.subghz_repeat = temp_data32;
-        }
-
-        if(!flipper_format_read_uint32(fff_settings, "SubGHz Ext Antenna", &temp_data32, 1)) {
-            FURI_LOG_W(TAG, "SETTINGS: Missing 'SubGHz Ext Antenna'");
-        } else {
-            app->settings.subghz_use_ext_antenna = temp_data32 == 1;
-        }
-
         if(!flipper_format_read_uint32(fff_settings, "IR Ext Module", &temp_data32, 1)) {
             FURI_LOG_W(TAG, "SETTINGS: Missing 'IR Ext Module'");
         } else {
@@ -155,6 +148,11 @@ void quac_save_settings(App* app) {
             FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Show Headers'");
             break;
         }
+        if(!flipper_format_write_uint32(
+               fff_settings, "SubGhz Duration", &app->settings.subghz_duration, 1)) {
+            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'SubGhz Duration'");
+            break;
+        }
         if(!flipper_format_write_uint32(
                fff_settings, "RFID Duration", &app->settings.rfid_duration, 1)) {
             FURI_LOG_E(TAG, "SETTINGS: Failed to write 'RFID Duration'");
@@ -170,16 +168,6 @@ void quac_save_settings(App* app) {
             FURI_LOG_E(TAG, "SETTINGS: Failed to write 'iButton Duration'");
             break;
         }
-        if(!flipper_format_write_uint32(
-               fff_settings, "SubGHz Repeat", &app->settings.subghz_repeat, 1)) {
-            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'SubGHz Repeat'");
-            break;
-        }
-        temp_data32 = app->settings.subghz_use_ext_antenna ? 1 : 0;
-        if(!flipper_format_write_uint32(fff_settings, "SubGHz Ext Antenna", &temp_data32, 1)) {
-            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'SubGHz Ext Antenna'");
-            break;
-        }
         temp_data32 = app->settings.ir_use_ext_module ? 1 : 0;
         if(!flipper_format_write_uint32(fff_settings, "IR Ext Module", &temp_data32, 1)) {
             FURI_LOG_E(TAG, "SETTINGS: Failed to write 'IR Ext Module'");

+ 2 - 3
scenes/scene_about.c

@@ -7,16 +7,15 @@
 #include "quac.h"
 #include "scenes.h"
 #include "scene_about.h"
-#include "../actions/action.h"
 #include "quac_icons.h"
 
 enum {
-    SceneActionRenameEvent,
+    SceneActionAboutEvent,
 };
 
 void scene_about_callback(void* context) {
     App* app = context;
-    view_dispatcher_send_custom_event(app->view_dispatcher, SceneActionRenameEvent);
+    view_dispatcher_send_custom_event(app->view_dispatcher, SceneActionAboutEvent);
 }
 
 void scene_about_on_enter(void* context) {

+ 0 - 1
scenes/scene_action_rename.c

@@ -7,7 +7,6 @@
 #include "quac.h"
 #include "scenes.h"
 #include "scene_action_rename.h"
-#include "../actions/action.h"
 
 #include <lib/toolbox/path.h>
 

+ 15 - 43
scenes/scene_settings.c

@@ -8,8 +8,6 @@
 #include "quac.h"
 #include "scenes.h"
 #include "scene_settings.h"
-#include "../actions/action.h"
-#include "../views/action_menu.h"
 #include "../quac_settings.h"
 
 #include <lib/toolbox/path.h>
@@ -19,7 +17,7 @@
 // dynamically know it's list index for our on_event method. However, we'll need to
 // hardcode the value..
 // TODO: Figure out a better way to do this
-#define SCENE_SETTINGS_ABOUT 10 // 11 items in our Settings list, so last index is 10
+#define SCENE_SETTINGS_ABOUT 8 // 9 items in our Settings list, so last index is 8
 
 static const char* const layout_text[2] = {"Vert", "Horiz"};
 static const uint32_t layout_value[2] = {QUAC_APP_PORTRAIT, QUAC_APP_LANDSCAPE};
@@ -49,19 +47,6 @@ static const uint32_t duration_value[V_DURATION_COUNT] = {
     10000,
 };
 
-#define V_REPEAT_COUNT 9
-static const char* const repeat_text[V_REPEAT_COUNT] = {
-    "1",
-    "2",
-    "3",
-    "5",
-    "8",
-    "10", // default
-    "15",
-    "20",
-    "50"};
-static const uint32_t repeat_value[V_REPEAT_COUNT] = {1, 2, 3, 5, 8, 10, 15, 20, 50};
-
 static const char* const disabled_enabled_text[2] = {"Disabled", "Enabled"};
 static const uint32_t disabled_enabled_value[2] = {false, true};
 
@@ -86,6 +71,13 @@ static void scene_settings_show_headers_changed(VariableItem* item) {
     app->settings.show_headers = show_offon_value[index];
 }
 
+static void scene_settings_subghz_duration_changed(VariableItem* item) {
+    App* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, duration_text[index]);
+    app->settings.subghz_duration = duration_value[index];
+}
+
 static void scene_settings_rfid_duration_changed(VariableItem* item) {
     App* app = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
@@ -107,20 +99,6 @@ static void scene_settings_ibutton_duration_changed(VariableItem* item) {
     app->settings.ibutton_duration = duration_value[index];
 }
 
-static void scene_settings_subghz_repeat_changed(VariableItem* item) {
-    App* app = variable_item_get_context(item);
-    uint8_t index = variable_item_get_current_value_index(item);
-    variable_item_set_current_value_text(item, repeat_text[index]);
-    app->settings.subghz_repeat = repeat_value[index];
-}
-
-static void scene_settings_subghz_ext_changed(VariableItem* item) {
-    App* app = variable_item_get_context(item);
-    uint8_t index = variable_item_get_current_value_index(item);
-    variable_item_set_current_value_text(item, disabled_enabled_text[index]);
-    app->settings.subghz_use_ext_antenna = disabled_enabled_value[index];
-}
-
 static void scene_settings_ir_ext_changed(VariableItem* item) {
     App* app = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
@@ -166,6 +144,13 @@ void scene_settings_on_enter(void* context) {
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, show_offon_text[value_index]);
 
+    item = variable_item_list_add(
+        vil, "SubGhz Duration", V_DURATION_COUNT, scene_settings_subghz_duration_changed, app);
+    value_index =
+        value_index_uint32(app->settings.subghz_duration, duration_value, V_DURATION_COUNT);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, duration_text[value_index]);
+
     item = variable_item_list_add(
         vil, "RFID Duration", V_DURATION_COUNT, scene_settings_rfid_duration_changed, app);
     value_index =
@@ -186,19 +171,6 @@ void scene_settings_on_enter(void* context) {
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, duration_text[value_index]);
 
-    item = variable_item_list_add(
-        vil, "SubGHz Repeat", V_REPEAT_COUNT, scene_settings_subghz_repeat_changed, app);
-    value_index = value_index_uint32(app->settings.subghz_repeat, repeat_value, V_REPEAT_COUNT);
-    variable_item_set_current_value_index(item, value_index);
-    variable_item_set_current_value_text(item, repeat_text[value_index]);
-
-    item =
-        variable_item_list_add(vil, "SubGHz Ext Ant", 2, scene_settings_subghz_ext_changed, app);
-    value_index =
-        value_index_uint32(app->settings.subghz_use_ext_antenna, disabled_enabled_value, 2);
-    variable_item_set_current_value_index(item, value_index);
-    variable_item_set_current_value_text(item, disabled_enabled_text[value_index]);
-
     item = variable_item_list_add(vil, "IR Ext Module", 2, scene_settings_ir_ext_changed, app);
     value_index = value_index_uint32(app->settings.ir_use_ext_module, disabled_enabled_value, 2);
     variable_item_set_current_value_index(item, value_index);

+ 1 - 0
scenes/scenes.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include <gui/scene_manager.h>
 typedef enum {
     QScene_Items,
     QScene_Settings,