Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IR: Easy Learn #350

Merged
merged 27 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ed80457
initial working commit
jaylikesbunda Jan 12, 2025
1a12840
update names + format
jaylikesbunda Jan 12, 2025
08d342a
add skip functionality
jaylikesbunda Jan 12, 2025
3ef4488
misc tweaks
jaylikesbunda Jan 12, 2025
7057463
change back gpio label
jaylikesbunda Jan 12, 2025
1be14dd
remove gpio setting changes
jaylikesbunda Jan 12, 2025
ef9d868
misc fixes
jaylikesbunda Jan 12, 2025
2a35845
bug fixes and polish
jaylikesbunda Jan 13, 2025
ec1b999
add subtitle button and reorganize order
jaylikesbunda Jan 21, 2025
5b54a76
Merge branch 'Next-Flip:dev' into infrared-learn
jaylikesbunda Jan 21, 2025
342eb3f
update ir settings to version 2
jaylikesbunda Jan 21, 2025
213733f
ir settings v1 migration support
jaylikesbunda Jan 21, 2025
ce8e73e
fixes
jaylikesbunda Jan 21, 2025
b742475
Merge branch 'Next-Flip:dev' into infrared-learn
jaylikesbunda Jan 21, 2025
3fc0fdb
format
jaylikesbunda Jan 21, 2025
dd13d4a
Merge branch 'infrared-learn' of https://github.com/jaylikesbunda/Mom…
jaylikesbunda Jan 21, 2025
c75f008
misc fixes
jaylikesbunda Jan 21, 2025
837b9ca
Merge remote-tracking branch 'mntm/dev' into mntm-pr-350
Willy-JL Jan 22, 2025
868d286
Simplify and standardize settings handling
Willy-JL Jan 22, 2025
358877c
Auto-calculate easy_mode_button_count
Willy-JL Jan 22, 2025
8198929
Case insensitive match existing remote buttons
Willy-JL Jan 22, 2025
1419f2d
Display button name more prominently
Willy-JL Jan 22, 2025
c4abc30
Sort submenu indexes and handling
Willy-JL Jan 22, 2025
cfaf24e
Fine to keep text highlighted
Willy-JL Jan 22, 2025
5c889d0
Some formatting for less conflicts
Willy-JL Jan 22, 2025
e602109
Not sure how these got lost kek
Willy-JL Jan 22, 2025
1a376a5
Update changelog
Willy-JL Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 9 additions & 12 deletions applications/main/infrared/infrared_app.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "infrared_app_i.h"

#include "infrared_settings.h"

#include <furi_hal_power.h>

#include <string.h>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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");
}
}
Expand Down
12 changes: 0 additions & 12 deletions applications/main/infrared/infrared_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,3 @@
* @brief InfraredApp opaque type declaration.
*/
typedef struct InfraredApp InfraredApp;

#include <storage/storage.h>
#include <furi_hal_infrared.h>

#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;
6 changes: 6 additions & 0 deletions applications/main/infrared/infrared_app_i.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions applications/main/infrared/infrared_settings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#pragma once

#include <storage/storage.h>
#include <furi_hal_infrared.h>
#include <toolbox/saved_struct.h>

#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);
}
163 changes: 153 additions & 10 deletions applications/main/infrared/scenes/infrared_scene_learn.c
Original file line number Diff line number Diff line change
@@ -1,23 +1,155 @@
#include "../infrared_app_i.h"
#include <dolphin/dolphin.h>

/* 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);

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, InfraredViewPopup);
view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewDialogEx);
}

bool infrared_scene_learn_on_event(void* context, SceneManagerEvent event) {
Expand All @@ -30,18 +162,29 @@ 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;
}

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading