Skip to content

Commit

Permalink
Infrared: Add option to "Load from Library File" for Universal Remotes (
Browse files Browse the repository at this point in the history
#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 <[email protected]>
  • Loading branch information
zxkmm and Willy-JL authored Oct 12, 2024
1 parent 925481f commit 2f22fad
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 5 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
1 change: 1 addition & 0 deletions applications/main/infrared/scenes/infrared_scene_config.h
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
118 changes: 118 additions & 0 deletions applications/main/infrared/scenes/infrared_scene_universal_from_file.c
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);
}

0 comments on commit 2f22fad

Please sign in to comment.