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

Infrared: Add option to "Load from Library File" for Universal Remotes #255

Merged
merged 25 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
69 changes: 68 additions & 1 deletion applications/main/infrared/infrared_brute_force.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

#include "infrared_signal.h"

#define TAG "InfraredBruteforce"

#define INFRARED_FILE_HEADER "IR signals file"
#define INFRARED_LIBRARY_HEADER "IR library file"
#define INFRARED_LIBRARY_VERSION (1)

typedef struct {
uint32_t index;
uint32_t count;
Expand Down Expand Up @@ -50,7 +56,9 @@ void infrared_brute_force_set_db_filename(InfraredBruteForce* brute_force, const
brute_force->db_filename = db_filename;
}

InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force) {
InfraredErrorCode infrared_brute_force_calculate_messages(
InfraredBruteForce* brute_force,
bool auto_detect_buttons) {
furi_assert(!brute_force->is_started);
furi_assert(brute_force->db_filename);
InfraredErrorCode error = InfraredErrorCodeNone;
Expand All @@ -66,14 +74,45 @@ InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* br
break;
}

uint32_t version;
// Temporarily use signal_name to get header info
if(!flipper_format_read_header(ff, signal_name, &version)) {
error = InfraredErrorCodeFileOperationFailed;
break;
}

if(furi_string_equal(signal_name, INFRARED_FILE_HEADER)) {
FURI_LOG_E(TAG, "Remote file can't be loaded in this context");
error = InfraredErrorCodeWrongFileType;
break;
}

if(!furi_string_equal(signal_name, INFRARED_LIBRARY_HEADER)) {
error = InfraredErrorCodeWrongFileType;
FURI_LOG_E(TAG, "Filetype unknown");
break;
}

if(version != INFRARED_LIBRARY_VERSION) {
error = InfraredErrorCodeWrongFileVersion;
FURI_LOG_E(TAG, "Wrong file version");
break;
}

bool signals_valid = false;
uint32_t auto_detect_button_index = 0;
while(infrared_signal_read_name(ff, signal_name) == InfraredErrorCodeNone) {
error = infrared_signal_read_body(signal, ff);
signals_valid = (!INFRARED_ERROR_PRESENT(error)) && infrared_signal_is_valid(signal);
if(!signals_valid) break;

InfraredBruteForceRecord* record =
InfraredBruteForceRecordDict_get(brute_force->records, signal_name);
if(!record && auto_detect_buttons) {
infrared_brute_force_add_record(
brute_force, auto_detect_button_index++, furi_string_get_cstr(signal_name));
record = InfraredBruteForceRecordDict_get(brute_force->records, signal_name);
}
if(record) { //-V547
++(record->count);
}
Expand Down Expand Up @@ -167,3 +206,31 @@ void infrared_brute_force_reset(InfraredBruteForce* brute_force) {
furi_assert(!brute_force->is_started);
InfraredBruteForceRecordDict_reset(brute_force->records);
}

size_t infrared_brute_force_get_button_count(const InfraredBruteForce* brute_force) {
size_t size = InfraredBruteForceRecordDict_size(brute_force->records);
return size;
}

const char*
infrared_brute_force_get_button_name(const InfraredBruteForce* brute_force, size_t index) {
if(index >= infrared_brute_force_get_button_count(brute_force)) {
return NULL;
}

InfraredBruteForceRecordDict_it_t it;
for(InfraredBruteForceRecordDict_it(it, brute_force->records);
!InfraredBruteForceRecordDict_end_p(it);
InfraredBruteForceRecordDict_next(it)) {
// Dict elements are unordered, they may be shuffled while adding elements, so the
// index used in add_record() may differ when iterating here, so we have to check
// the stored index not "position" index
const InfraredBruteForceRecordDict_itref_t* pair = InfraredBruteForceRecordDict_cref(it);
if(pair->value.index == index) {
const char* button_name = furi_string_get_cstr(pair->key);
return button_name;
}
}

return NULL; //just as fallback
}
26 changes: 25 additions & 1 deletion applications/main/infrared/infrared_brute_force.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "infrared_error_code.h"

/**
Expand Down Expand Up @@ -46,9 +47,12 @@ void infrared_brute_force_set_db_filename(InfraredBruteForce* brute_force, const
* a infrared_brute_force_set_db_filename() call.
*
* @param[in,out] brute_force pointer to the instance to be updated.
* @param[in] auto_detect_buttons bool whether to automatically register newly discovered buttons.
* @returns InfraredErrorCodeNone on success, otherwise error code.
*/
InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force);
InfraredErrorCode infrared_brute_force_calculate_messages(
InfraredBruteForce* brute_force,
bool auto_detect_buttons);

/**
* @brief Start transmitting signals from a category stored in an InfraredBruteForce's instance dictionary.
Expand Down Expand Up @@ -109,3 +113,23 @@ void infrared_brute_force_add_record(
* @param[in,out] brute_force pointer to the instance to be reset.
*/
void infrared_brute_force_reset(InfraredBruteForce* brute_force);

/**
* @brief Get the total number of unique button names in the database, for example,
* if a button name is "Power" and it appears 3 times in the db, then the
* db_size is 1, instead of 3.
*
* @param[in] brute_force pointer to the InfraredBruteForce instance.
* @return size_t number of unique button names.
*/
size_t infrared_brute_force_get_button_count(const InfraredBruteForce* brute_force);

/**
* @brief Get the button name at the specified index.
*
* @param[in] brute_force pointer to the InfraredBruteForce instance.
* @param[in] index index of the button name to retrieve.
* @return const char* button name, or NULL if index is out of range.
*/
const char*
infrared_brute_force_get_button_name(const InfraredBruteForce* brute_force, size_t index);
2 changes: 1 addition & 1 deletion applications/main/infrared/infrared_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ static void
printf("Missing signal name.\r\n");
break;
}
if(infrared_brute_force_calculate_messages(brute_force) != InfraredErrorCodeNone) {
if(infrared_brute_force_calculate_messages(brute_force, false) != InfraredErrorCodeNone) {
printf("Invalid remote name.\r\n");
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ static void infrared_scene_universal_common_hide_popup(InfraredApp* infrared) {

static int32_t infrared_scene_universal_common_task_callback(void* context) {
InfraredApp* infrared = context;
const InfraredErrorCode error = infrared_brute_force_calculate_messages(infrared->brute_force);
const InfraredErrorCode error =
infrared_brute_force_calculate_messages(infrared->brute_force, false);
view_dispatcher_send_custom_event(
infrared->view_dispatcher,
infrared_custom_event_pack(InfraredCustomEventTypeTaskFinished, 0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ADD_SCENE(infrared, universal_fan, UniversalFan)
ADD_SCENE(infrared, universal_bluray, UniversalBluray)
ADD_SCENE(infrared, universal_monitor, UniversalMonitor)
ADD_SCENE(infrared, universal_digital_sign, UniversalDigitalSign)
ADD_SCENE(infrared, universal_from_file, UniversalFromFile)
ADD_SCENE(infrared, gpio_settings, GpioSettings)
ADD_SCENE(infrared, debug, Debug)
ADD_SCENE(infrared, error_databases, ErrorDatabases)
Expand Down
4 changes: 4 additions & 0 deletions applications/main/infrared/scenes/infrared_scene_start.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ bool infrared_scene_start_on_event(void* context, SceneManagerEvent event) {
const uint32_t submenu_index = event.event;
scene_manager_set_scene_state(scene_manager, InfraredSceneStart, submenu_index);
if(submenu_index == SubmenuIndexUniversalRemotes) {
// Set file_path only once here so repeated usages of
// "Load from Library File" have file browser focused on
// last selected file, feels more intuitive
furi_string_set(infrared->file_path, INFRARED_APP_FOLDER);
scene_manager_next_scene(scene_manager, InfraredSceneUniversal);
} else if(
submenu_index == SubmenuIndexLearnNewRemote ||
Expand Down
11 changes: 11 additions & 0 deletions applications/main/infrared/scenes/infrared_scene_universal.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ typedef enum {
SubmenuIndexUniversalBluray,
SubmenuIndexUniversalMonitor,
SubmenuIndexUniversalDigitalSign,
SubmenuIndexUniversalFromFile,
} SubmenuIndex;

static void infrared_scene_universal_submenu_callback(void* context, uint32_t index) {
Expand Down Expand Up @@ -84,6 +85,13 @@ void infrared_scene_universal_on_enter(void* context) {
infrared_scene_universal_submenu_callback,
context);

submenu_add_item(
submenu,
"Load from Library File",
SubmenuIndexUniversalFromFile,
infrared_scene_universal_submenu_callback,
context);

submenu_set_selected_item(
submenu, scene_manager_get_scene_state(infrared->scene_manager, InfraredSceneUniversal));

Expand Down Expand Up @@ -123,6 +131,9 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) {
} else if(event.event == SubmenuIndexUniversalDigitalSign) {
scene_manager_next_scene(scene_manager, InfraredSceneUniversalDigitalSign);
consumed = true;
} else if(event.event == SubmenuIndexUniversalFromFile) {
scene_manager_next_scene(scene_manager, InfraredSceneUniversalFromFile);
consumed = true;
}
scene_manager_set_scene_state(scene_manager, InfraredSceneUniversal, event.event);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "../infrared_app_i.h"

#include "common/infrared_scene_universal_common.h"

static void
infrared_scene_universal_from_file_item_callback(void* context, int32_t index, InputType type) {
if(type == InputTypeRelease) {
InfraredApp* infrared = context;
uint32_t event = infrared_custom_event_pack(InfraredCustomEventTypeButtonSelected, index);
view_dispatcher_send_custom_event(infrared->view_dispatcher, event);
}
}

static int32_t infrared_scene_universal_from_file_task_callback(void* context) {
InfraredApp* infrared = context;
ButtonMenu* button_menu = infrared->button_menu;
InfraredBruteForce* brute_force = infrared->brute_force;
const InfraredErrorCode error =
infrared_brute_force_calculate_messages(infrared->brute_force, true);

if(!INFRARED_ERROR_PRESENT(error)) {
// add btns
for(size_t i = 0; i < infrared_brute_force_get_button_count(brute_force); ++i) {
const char* button_name = infrared_brute_force_get_button_name(brute_force, i);
button_menu_add_item(
button_menu,
button_name,
i,
infrared_scene_universal_from_file_item_callback,
ButtonMenuItemTypeCommon,
infrared);
}
}

view_dispatcher_send_custom_event(
infrared->view_dispatcher,
infrared_custom_event_pack(InfraredCustomEventTypeTaskFinished, 0));

return error;
}

void infrared_scene_universal_from_file_on_enter(void* context) {
InfraredApp* infrared = context;
ButtonMenu* button_menu = infrared->button_menu;
InfraredBruteForce* brute_force = infrared->brute_force;

DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(&browser_options, INFRARED_APP_EXTENSION, &I_ir_10px);
browser_options.base_path = INFRARED_APP_FOLDER;
browser_options.skip_assets = false;
if(!dialog_file_browser_show(
infrared->dialogs, infrared->file_path, infrared->file_path, &browser_options)) {
scene_manager_previous_scene(infrared->scene_manager);
return;
}

infrared_brute_force_set_db_filename(brute_force, furi_string_get_cstr(infrared->file_path));

// File name in header
// Using c-string functions on FuriString is a bad idea but file_path is not modified
// for the lifetime of this scene so it should be fine
const char* file_name = strrchr(furi_string_get_cstr(infrared->file_path), '/');
if(file_name) {
file_name++; // skip dir seperator
} else {
file_name = furi_string_get_cstr(infrared->file_path); // fallback
}
button_menu_set_header(button_menu, file_name);

// Can't use infrared_scene_universal_common_on_enter() since we use ButtonMenu not ButtonPanel
view_set_orientation(view_stack_get_view(infrared->view_stack), ViewOrientationVertical);
view_stack_add_view(infrared->view_stack, button_menu_get_view(infrared->button_menu));

// Load universal remote data in background
infrared_blocking_task_start(infrared, infrared_scene_universal_from_file_task_callback);
}

bool infrared_scene_universal_from_file_on_event(void* context, SceneManagerEvent event) {
InfraredApp* infrared = context;
SceneManager* scene_manager = infrared->scene_manager;
InfraredBruteForce* brute_force = infrared->brute_force;

// Only override InfraredCustomEventTypeTaskFinished on error condition
if(!infrared_brute_force_is_started(brute_force) &&
event.type == SceneManagerEventTypeCustom) {
uint16_t event_type;
int16_t event_value;
infrared_custom_event_unpack(event.event, &event_type, &event_value);
if(event_type == InfraredCustomEventTypeTaskFinished) {
const InfraredErrorCode task_error = infrared_blocking_task_finalize(infrared);

if(INFRARED_ERROR_PRESENT(task_error)) {
bool wrong_file_type =
INFRARED_ERROR_CHECK(task_error, InfraredErrorCodeWrongFileType);
const char* format = wrong_file_type ?
"Remote file\n\"%s\" can't be openned as a library" :
"Failed to load\n\"%s\"";

infrared_show_error_message(
infrared, format, furi_string_get_cstr(infrared->file_path));
scene_manager_previous_scene(scene_manager);
return true;
}
}
}

// Use common function for all other functionality
return infrared_scene_universal_common_on_event(context, event);
}

void infrared_scene_universal_from_file_on_exit(void* context) {
// Can't use infrared_scene_universal_common_on_exit() since we use ButtonMenu not ButtonPanel
InfraredApp* infrared = context;
ButtonMenu* button_menu = infrared->button_menu;
view_stack_remove_view(infrared->view_stack, button_menu_get_view(button_menu));
infrared_brute_force_reset(infrared->brute_force);
button_menu_reset(button_menu);
}
Loading