diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c656061..329dc30d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - GPIO: TEA5767 FM Radio (by @coolshrimp) - NFC: Metroflip (by @luu176) - USB: USB Game Controller (by @expected-ingot) +- Infrared: Easy Learn mode to quickly save buttons without typing (#350 by @jaylikesbunda) - Archive: Setting to show dynamic path in file browser statusbar (#322 by @956MB) - CLI: Add `clear` and `cls` commands, add `did you mean ...?` command suggestion (#342 by @dexvleads) - Main Menu: Add coverflow menu style (#314 by @CodyTolene) diff --git a/applications/external b/applications/external index b1d515897..337177af6 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit b1d5158975f0edf35ca59bf327b4c23b024474fb +Subproject commit 337177af6444a1d55c2e9a7002c5b5c08c8eee55 diff --git a/applications/main/infrared/infrared_app.c b/applications/main/infrared/infrared_app.c index 4bc4937df..43f6e7817 100644 --- a/applications/main/infrared/infrared_app.c +++ b/applications/main/infrared/infrared_app.c @@ -1,5 +1,7 @@ #include "infrared_app_i.h" +#include "infrared_settings.h" + #include #include @@ -153,6 +155,9 @@ static InfraredApp* infrared_alloc(void) { InfraredAppState* app_state = &infrared->app_state; app_state->is_learning_new_remote = false; app_state->is_debug_enabled = furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); + app_state->is_transmitting = false; + app_state->is_otg_enabled = false; + app_state->is_easy_mode = false; app_state->edit_target = InfraredEditTargetNone; app_state->edit_mode = InfraredEditModeNone; app_state->current_button_index = InfraredButtonIndexNone; @@ -503,12 +508,7 @@ void infrared_enable_otg(InfraredApp* infrared, bool enable) { static void infrared_load_settings(InfraredApp* infrared) { InfraredSettings settings = {0}; - if(!saved_struct_load( - INFRARED_SETTINGS_PATH, - &settings, - sizeof(InfraredSettings), - INFRARED_SETTINGS_MAGIC, - INFRARED_SETTINGS_VERSION)) { + if(!infrared_settings_load(&settings)) { FURI_LOG_D(TAG, "Failed to load settings, using defaults"); // infrared_save_settings(infrared); } @@ -517,20 +517,17 @@ static void infrared_load_settings(InfraredApp* infrared) { if(settings.tx_pin < FuriHalInfraredTxPinMax) { infrared_enable_otg(infrared, settings.otg_enabled); } + infrared->app_state.is_easy_mode = settings.easy_mode; } void infrared_save_settings(InfraredApp* infrared) { InfraredSettings settings = { .tx_pin = infrared->app_state.tx_pin, .otg_enabled = infrared->app_state.is_otg_enabled, + .easy_mode = infrared->app_state.is_easy_mode, }; - if(!saved_struct_save( - INFRARED_SETTINGS_PATH, - &settings, - sizeof(InfraredSettings), - INFRARED_SETTINGS_MAGIC, - INFRARED_SETTINGS_VERSION)) { + if(!infrared_settings_save(&settings)) { FURI_LOG_E(TAG, "Failed to save settings"); } } diff --git a/applications/main/infrared/infrared_app.h b/applications/main/infrared/infrared_app.h index fedfd8af9..a6f87402a 100644 --- a/applications/main/infrared/infrared_app.h +++ b/applications/main/infrared/infrared_app.h @@ -13,15 +13,3 @@ * @brief InfraredApp opaque type declaration. */ typedef struct InfraredApp InfraredApp; - -#include -#include - -#define INFRARED_SETTINGS_PATH INT_PATH(".infrared.settings") -#define INFRARED_SETTINGS_VERSION (1) -#define INFRARED_SETTINGS_MAGIC (0x1F) - -typedef struct { - FuriHalInfraredTxPin tx_pin; - bool otg_enabled; -} InfraredSettings; diff --git a/applications/main/infrared/infrared_app_i.h b/applications/main/infrared/infrared_app_i.h index 75d8502f2..b078429b3 100644 --- a/applications/main/infrared/infrared_app_i.h +++ b/applications/main/infrared/infrared_app_i.h @@ -52,6 +52,10 @@ #define INFRARED_DEFAULT_REMOTE_NAME "Remote" #define INFRARED_LOG_TAG "InfraredApp" +/* Button names for easy mode */ +extern const char* const easy_mode_button_names[]; +extern const size_t easy_mode_button_count; // Number of buttons in the array + /** * @brief Enumeration of invalid remote button indices. */ @@ -85,9 +89,11 @@ typedef struct { bool is_debug_enabled; /**< Whether to enable or disable debugging features. */ bool is_transmitting; /**< Whether a signal is currently being transmitted. */ bool is_otg_enabled; /**< Whether OTG power (external 5V) is enabled. */ + bool is_easy_mode; /**< Whether easy learning mode is enabled. */ InfraredEditTarget edit_target : 8; /**< Selected editing target (a remote or a button). */ InfraredEditMode edit_mode : 8; /**< Selected editing operation (rename or delete). */ int32_t current_button_index; /**< Selected button index (move destination). */ + int32_t existing_remote_button_index; /**< Existing remote's current button index (easy mode). */ int32_t prev_button_index; /**< Previous button index (move source). */ uint32_t last_transmit_time; /**< Lat time a signal was transmitted. */ FuriHalInfraredTxPin tx_pin; diff --git a/applications/main/infrared/infrared_settings.h b/applications/main/infrared/infrared_settings.h new file mode 100644 index 000000000..bb9fa0ec3 --- /dev/null +++ b/applications/main/infrared/infrared_settings.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#define INFRARED_SETTINGS_PATH INT_PATH(".infrared.settings") +#define INFRARED_SETTINGS_VERSION (2) +#define INFRARED_SETTINGS_MAGIC (0x1F) + +typedef struct { + FuriHalInfraredTxPin tx_pin; + bool otg_enabled; + bool easy_mode; +} InfraredSettings; + +typedef struct { + FuriHalInfraredTxPin tx_pin; + bool otg_enabled; +} _InfraredSettingsV1; + +bool infrared_settings_load(InfraredSettings* settings) { + // Default load + if(saved_struct_load( + INFRARED_SETTINGS_PATH, + settings, + sizeof(*settings), + INFRARED_SETTINGS_MAGIC, + INFRARED_SETTINGS_VERSION)) { + return true; + } + + // Set defaults + settings->tx_pin = FuriHalInfraredTxPinInternal; + settings->otg_enabled = false; + settings->easy_mode = false; + + // Try to migrate + uint8_t magic, version; + if(saved_struct_get_metadata(INFRARED_SETTINGS_PATH, &magic, &version, NULL) && + magic == INFRARED_SETTINGS_MAGIC) { + _InfraredSettingsV1 v1; + + if(version == 1 && + saved_struct_load(INFRARED_SETTINGS_PATH, &v1, sizeof(v1), magic, version)) { + settings->tx_pin = v1.tx_pin; + settings->otg_enabled = v1.otg_enabled; + return true; + } + } + + return false; +} + +bool infrared_settings_save(InfraredSettings* settings) { + return saved_struct_save( + INFRARED_SETTINGS_PATH, + settings, + sizeof(*settings), + INFRARED_SETTINGS_MAGIC, + INFRARED_SETTINGS_VERSION); +} diff --git a/applications/main/infrared/scenes/infrared_scene_learn.c b/applications/main/infrared/scenes/infrared_scene_learn.c index bcd0a2cd0..e19624d20 100644 --- a/applications/main/infrared/scenes/infrared_scene_learn.c +++ b/applications/main/infrared/scenes/infrared_scene_learn.c @@ -1,23 +1,155 @@ #include "../infrared_app_i.h" #include +/* Button names for easy mode */ +const char* const easy_mode_button_names[] = {"Power", "Vol_up", "Vol_dn", "Mute", "Ch_up", + "Ch_dn", "Ok", "Up", "Down", "Left", + "Right", "Menu", "Back", "Play", "Pause", + "Stop", "Next", "Prev", "FF", "Rew", + "Input", "Exit", "Eject", "Subtitle"}; +const size_t easy_mode_button_count = COUNT_OF(easy_mode_button_names); + +static void infrared_scene_learn_dialog_result_callback(DialogExResult result, void* context) { + InfraredApp* infrared = context; + view_dispatcher_send_custom_event(infrared->view_dispatcher, result); +} + +static bool infrared_scene_learn_get_next_name( + InfraredApp* infrared, + int32_t start_index, + int32_t* next_index) { + if(!infrared->remote) return false; + + // Search through remaining button names to find one that doesn't exist + FuriString* name = furi_string_alloc(); + for(int32_t i = start_index; i < (int32_t)easy_mode_button_count; i++) { + furi_string_set(name, easy_mode_button_names[i]); + bool name_exists = false; + + // Check if this name already exists in remote + for(size_t j = 0; j < infrared_remote_get_signal_count(infrared->remote); j++) { + if(furi_string_cmpi(name, infrared_remote_get_signal_name(infrared->remote, j)) == 0) { + name_exists = true; + break; + } + } + + // If we found a name that doesn't exist, return it + if(!name_exists) { + *next_index = i; + return true; + } + } + furi_string_free(name); + + return false; +} + +static void infrared_scene_learn_update_button_name(InfraredApp* infrared, bool increment) { + DialogEx* dialog_ex = infrared->dialog_ex; + int32_t button_index; + + if(infrared->app_state.is_learning_new_remote) { + // For new remotes, use current_button_index directly + button_index = infrared->app_state.current_button_index; + if(increment) { + // Only increment if we haven't reached the last button + if(button_index + 1 < (int32_t)easy_mode_button_count) { + button_index++; + infrared->app_state.current_button_index = button_index; + } + } + } else if(infrared->remote) { + // For existing remotes, find next available button name + button_index = infrared->app_state.existing_remote_button_index; + if(increment) { + int32_t next_index; + if(infrared_scene_learn_get_next_name(infrared, button_index + 1, &next_index)) { + button_index = next_index; + infrared->app_state.existing_remote_button_index = button_index; + } + } + } else { + button_index = 0; + } + + // Ensure button_index is valid + if(button_index < 0) button_index = 0; + if(button_index >= (int32_t)easy_mode_button_count) { + button_index = (int32_t)easy_mode_button_count - 1; + } + + // Now we know button_index is valid, use it to get the name + const char* button_name = easy_mode_button_names[button_index]; + dialog_ex_set_text( + dialog_ex, "Point remote at IR port\nand press button:", 5, 10, AlignLeft, AlignCenter); + dialog_ex_set_header(dialog_ex, button_name, 78, 11, AlignLeft, AlignTop); + + // For existing remotes, check if there are any more buttons to add + bool has_more_buttons = false; + if(!infrared->app_state.is_learning_new_remote && infrared->remote) { + int32_t next_index; + has_more_buttons = + infrared_scene_learn_get_next_name(infrared, button_index + 1, &next_index); + } else { + has_more_buttons = (button_index + 1 < (int32_t)easy_mode_button_count); + } + + // Show/hide skip button based on whether there are more buttons + if(!has_more_buttons) { + dialog_ex_set_center_button_text(dialog_ex, NULL); + } else { + dialog_ex_set_center_button_text(dialog_ex, "Skip"); + } +} + void infrared_scene_learn_on_enter(void* context) { InfraredApp* infrared = context; - Popup* popup = infrared->popup; + DialogEx* dialog_ex = infrared->dialog_ex; InfraredWorker* worker = infrared->worker; + // Initialize or validate current_button_index + if(infrared->app_state.is_learning_new_remote) { + // If index is beyond our predefined names, reset it + if(infrared->app_state.current_button_index >= (int32_t)easy_mode_button_count) { + infrared->app_state.current_button_index = 0; + } + } else { + // For existing remotes, find first missing button name + int32_t next_index; + if(infrared_scene_learn_get_next_name(infrared, 0, &next_index)) { + infrared->app_state.existing_remote_button_index = next_index; + } else { + // If no missing buttons found, start at beginning + infrared->app_state.existing_remote_button_index = 0; + } + } + infrared_worker_rx_set_received_signal_callback( worker, infrared_signal_received_callback, context); infrared_worker_rx_start(worker); infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStartRead); - popup_set_icon(popup, 0, 32, &I_InfraredLearnShort_128x31); - popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignCenter); - popup_set_text( - popup, "Point the remote at IR port\nand push the button", 5, 10, AlignLeft, AlignCenter); - popup_set_callback(popup, NULL); + dialog_ex_set_icon(dialog_ex, 0, 32, &I_InfraredLearnShort_128x31); + dialog_ex_set_header(dialog_ex, NULL, 0, 0, AlignCenter, AlignCenter); - view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewPopup); + if(infrared->app_state.is_easy_mode) { + infrared_scene_learn_update_button_name(infrared, false); + dialog_ex_set_icon(dialog_ex, 0, 22, &I_InfraredLearnShort_128x31); + } else { + dialog_ex_set_text( + dialog_ex, + "Point the remote at IR port\nand push the button", + 5, + 13, + AlignLeft, + AlignCenter); + } + + dialog_ex_set_context(dialog_ex, context); + dialog_ex_set_result_callback(dialog_ex, infrared_scene_learn_dialog_result_callback); + + view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewDialogEx); } bool infrared_scene_learn_on_event(void* context, SceneManagerEvent event) { @@ -30,7 +162,19 @@ bool infrared_scene_learn_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(infrared->scene_manager, InfraredSceneLearnSuccess); dolphin_deed(DolphinDeedIrLearnSuccess); consumed = true; + } else if(event.event == DialogExResultCenter && infrared->app_state.is_easy_mode) { + // Update with increment when skipping + infrared_scene_learn_update_button_name(infrared, true); + consumed = true; } + } else if(event.type == SceneManagerEventTypeBack) { + // Reset button indices when exiting learn mode completely + if(infrared->app_state.is_learning_new_remote) { + infrared->app_state.current_button_index = 0; + } else { + infrared->app_state.existing_remote_button_index = 0; + } + consumed = false; } return consumed; @@ -38,10 +182,9 @@ bool infrared_scene_learn_on_event(void* context, SceneManagerEvent event) { void infrared_scene_learn_on_exit(void* context) { InfraredApp* infrared = context; - Popup* popup = infrared->popup; + DialogEx* dialog_ex = infrared->dialog_ex; infrared_worker_rx_set_received_signal_callback(infrared->worker, NULL, NULL); infrared_worker_rx_stop(infrared->worker); infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStop); - popup_set_icon(popup, 0, 0, NULL); - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignCenter); + dialog_ex_reset(dialog_ex); } diff --git a/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c b/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c index 7ffc9644b..bd9b2182c 100644 --- a/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c +++ b/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c @@ -6,7 +6,24 @@ void infrared_scene_learn_enter_name_on_enter(void* context) { TextInput* text_input = infrared->text_input; InfraredSignal* signal = infrared->current_signal; - if(infrared_signal_is_raw(signal)) { + if(infrared->app_state.is_easy_mode) { + // In easy mode, use predefined names based on button index + int32_t button_index; + if(infrared->app_state.is_learning_new_remote) { + button_index = infrared->app_state.current_button_index; + } else { + button_index = infrared->app_state.existing_remote_button_index; + } + + // Ensure button_index is valid + if(button_index < 0) button_index = 0; + if(button_index >= (int32_t)easy_mode_button_count) { + button_index = (int32_t)easy_mode_button_count - 1; + } + + // Always use predefined names in easy mode + infrared_text_store_set(infrared, 0, "%s", easy_mode_button_names[button_index]); + } else if(infrared_signal_is_raw(signal)) { const InfraredRawSignal* raw = infrared_signal_get_raw_signal(signal); infrared_text_store_set(infrared, 0, "RAW_%zu", raw->timings_size); } else { diff --git a/applications/main/infrared/scenes/infrared_scene_start.c b/applications/main/infrared/scenes/infrared_scene_start.c index 11944df19..76c5f45be 100644 --- a/applications/main/infrared/scenes/infrared_scene_start.c +++ b/applications/main/infrared/scenes/infrared_scene_start.c @@ -5,6 +5,7 @@ enum SubmenuIndex { SubmenuIndexLearnNewRemote, SubmenuIndexSavedRemotes, SubmenuIndexGpioSettings, + SubmenuIndexEasyLearn, SubmenuIndexLearnNewRemoteRaw, SubmenuIndexDebug }; @@ -44,6 +45,19 @@ void infrared_scene_start_on_enter(void* context) { infrared_scene_start_submenu_callback, infrared); + char easy_learn_text[24]; + snprintf( + easy_learn_text, + sizeof(easy_learn_text), + "Easy Learn [%s]", + infrared->app_state.is_easy_mode ? "X" : " "); + submenu_add_item( + submenu, + easy_learn_text, + SubmenuIndexEasyLearn, + infrared_scene_start_submenu_callback, + infrared); + submenu_add_lockable_item( submenu, "Learn New Remote RAW", @@ -70,7 +84,7 @@ void infrared_scene_start_on_enter(void* context) { const uint32_t submenu_index = scene_manager_get_scene_state(scene_manager, InfraredSceneStart); submenu_set_selected_item(submenu, submenu_index); - scene_manager_set_scene_state(scene_manager, InfraredSceneStart, SubmenuIndexUniversalRemotes); + // scene_manager_set_scene_state(scene_manager, InfraredSceneStart, SubmenuIndexUniversalRemotes); view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewSubmenu); } @@ -104,6 +118,17 @@ bool infrared_scene_start_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(scene_manager, InfraredSceneRemoteList); } else if(submenu_index == SubmenuIndexGpioSettings) { scene_manager_next_scene(scene_manager, InfraredSceneGpioSettings); + } else if(submenu_index == SubmenuIndexEasyLearn) { + infrared->app_state.is_easy_mode = !infrared->app_state.is_easy_mode; + infrared_save_settings(infrared); + // Update the menu item text without scene transition + char easy_learn_text[24]; + snprintf( + easy_learn_text, + sizeof(easy_learn_text), + "Easy Learn [%s]", + infrared->app_state.is_easy_mode ? "X" : " "); + submenu_change_item_label(infrared->submenu, SubmenuIndexEasyLearn, easy_learn_text); } else if(submenu_index == SubmenuIndexDebug) { scene_manager_next_scene(scene_manager, InfraredSceneDebug); } diff --git a/furi/flipper.c b/furi/flipper.c index d04ccfb55..1b836f657 100644 --- a/furi/flipper.c +++ b/furi/flipper.c @@ -50,7 +50,7 @@ static void flipper_print_version(const char* target, const Version* version) { #include #include #include -#include +#include #include void flipper_migrate_files() {