From 2f22fad58bdbc4135f9ff1791bbf93778fff51e4 Mon Sep 17 00:00:00 2001 From: sommermorgentraum <24917424+zxkmm@users.noreply.github.com> Date: Sat, 12 Oct 2024 09:08:20 +0800 Subject: [PATCH] Infrared: Add option to "Load from Library File" for Universal Remotes (#255) * init * comments * remove trash * remove code that mistakenly added from merging conflicts * remove code that mistakenly added from merging conflicts * format * remove header that added during debugging * ecit name * Revert some whitespace changes to avoid future conflicts * get_button_count() * Use same index values * Use common functions where possible * Unroll long if into guard check * Fix furi check failed due to inflated button index * Show "assets" folders * Load DB file only once and show loading animation * Add bool for auto_detect_buttons * Show error when tryingto load remote file as universal library * Remove unnecessary includes * Fix inputs * more_devices -> from_file * Consistency * Remember last selected library file * Update changelog --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> --- CHANGELOG.md | 4 +- .../main/infrared/infrared_brute_force.c | 69 +++++++++- .../main/infrared/infrared_brute_force.h | 26 +++- applications/main/infrared/infrared_cli.c | 2 +- .../common/infrared_scene_universal_common.c | 3 +- .../infrared/scenes/infrared_scene_config.h | 1 + .../infrared/scenes/infrared_scene_start.c | 4 + .../scenes/infrared_scene_universal.c | 11 ++ .../infrared_scene_universal_from_file.c | 118 ++++++++++++++++++ 9 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 applications/main/infrared/scenes/infrared_scene_universal_from_file.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 960e17a5a6..fecfc23582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,9 @@ - Static encrypted backdoor support: collects static encrypted nonces to be cracked by MFKey using NXP/Fudan backdoor, allowing key recovery of all non-hardened MIFARE Classic tags on-device - Add SmartRider Parser (#203 by @jaylikesbunda) - Add API to enforce ISO15693 mode (#225 by @aaronjamt) -- Infrared: Bluray/DVD Universal Remote (#250 by @jaylikesbunda) +- Infrared: + - Bluray/DVD Universal Remote (#250 by @jaylikesbunda) + - Option to "Load from Library File" for Universal Remotes (#255 by @zxkmm) - Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) diff --git a/applications/main/infrared/infrared_brute_force.c b/applications/main/infrared/infrared_brute_force.c index 8c7422d5ef..e4525f868d 100644 --- a/applications/main/infrared/infrared_brute_force.c +++ b/applications/main/infrared/infrared_brute_force.c @@ -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; @@ -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; @@ -66,7 +74,33 @@ 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); @@ -74,6 +108,11 @@ InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* br 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); } @@ -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 +} diff --git a/applications/main/infrared/infrared_brute_force.h b/applications/main/infrared/infrared_brute_force.h index 8796422576..c1985ee3ff 100644 --- a/applications/main/infrared/infrared_brute_force.h +++ b/applications/main/infrared/infrared_brute_force.h @@ -10,6 +10,7 @@ #include #include +#include #include "infrared_error_code.h" /** @@ -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. @@ -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); diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index cdd5b9a118..d4668d699c 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -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; } diff --git a/applications/main/infrared/scenes/common/infrared_scene_universal_common.c b/applications/main/infrared/scenes/common/infrared_scene_universal_common.c index a52f141c47..dbccabb9b9 100644 --- a/applications/main/infrared/scenes/common/infrared_scene_universal_common.c +++ b/applications/main/infrared/scenes/common/infrared_scene_universal_common.c @@ -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)); diff --git a/applications/main/infrared/scenes/infrared_scene_config.h b/applications/main/infrared/scenes/infrared_scene_config.h index 739cab6bb4..074fc8ca2d 100644 --- a/applications/main/infrared/scenes/infrared_scene_config.h +++ b/applications/main/infrared/scenes/infrared_scene_config.h @@ -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) diff --git a/applications/main/infrared/scenes/infrared_scene_start.c b/applications/main/infrared/scenes/infrared_scene_start.c index 3a9512a619..11944df19e 100644 --- a/applications/main/infrared/scenes/infrared_scene_start.c +++ b/applications/main/infrared/scenes/infrared_scene_start.c @@ -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 || diff --git a/applications/main/infrared/scenes/infrared_scene_universal.c b/applications/main/infrared/scenes/infrared_scene_universal.c index 3c06fc7cc9..3616fe06ad 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal.c +++ b/applications/main/infrared/scenes/infrared_scene_universal.c @@ -10,6 +10,7 @@ typedef enum { SubmenuIndexUniversalBluray, SubmenuIndexUniversalMonitor, SubmenuIndexUniversalDigitalSign, + SubmenuIndexUniversalFromFile, } SubmenuIndex; static void infrared_scene_universal_submenu_callback(void* context, uint32_t index) { @@ -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)); @@ -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); } diff --git a/applications/main/infrared/scenes/infrared_scene_universal_from_file.c b/applications/main/infrared/scenes/infrared_scene_universal_from_file.c new file mode 100644 index 0000000000..d6a7190414 --- /dev/null +++ b/applications/main/infrared/scenes/infrared_scene_universal_from_file.c @@ -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); +}