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: Metadata Support #299

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 38 additions & 27 deletions applications/main/infrared/infrared_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ static InfraredApp* infrared_alloc(void) {
infrared->button_panel = button_panel_alloc();
infrared->progress = infrared_progress_view_alloc();

infrared->widget = widget_alloc();
view_dispatcher_add_view(
view_dispatcher, InfraredViewWidget, widget_get_view(infrared->widget));

return infrared;
}

Expand All @@ -243,58 +247,65 @@ static void infrared_free(InfraredApp* infrared) {
infrared->rpc_ctx = NULL;
}

view_dispatcher_remove_view(view_dispatcher, InfraredViewSubmenu);
submenu_free(infrared->submenu);
// First remove all views from dispatcher
if(view_dispatcher) {
view_dispatcher_remove_view(view_dispatcher, InfraredViewSubmenu);
view_dispatcher_remove_view(view_dispatcher, InfraredViewTextInput);
view_dispatcher_remove_view(view_dispatcher, InfraredViewDialogEx);
view_dispatcher_remove_view(view_dispatcher, InfraredViewButtonMenu);
view_dispatcher_remove_view(view_dispatcher, InfraredViewPopup);
view_dispatcher_remove_view(view_dispatcher, InfraredViewVariableList);
view_dispatcher_remove_view(view_dispatcher, InfraredViewStack);
view_dispatcher_remove_view(view_dispatcher, InfraredViewMove);
view_dispatcher_remove_view(view_dispatcher, InfraredViewLoading);
view_dispatcher_remove_view(view_dispatcher, InfraredViewWidget);
if(app_state->is_debug_enabled) {
view_dispatcher_remove_view(view_dispatcher, InfraredViewDebugView);
}
}

view_dispatcher_remove_view(view_dispatcher, InfraredViewTextInput);
// Then free all views
submenu_free(infrared->submenu);
text_input_free(infrared->text_input);

view_dispatcher_remove_view(view_dispatcher, InfraredViewDialogEx);
dialog_ex_free(infrared->dialog_ex);

view_dispatcher_remove_view(view_dispatcher, InfraredViewButtonMenu);
button_menu_free(infrared->button_menu);

view_dispatcher_remove_view(view_dispatcher, InfraredViewPopup);
popup_free(infrared->popup);

view_dispatcher_remove_view(view_dispatcher, InfraredViewVariableList);
variable_item_list_free(infrared->var_item_list);

view_dispatcher_remove_view(view_dispatcher, InfraredViewStack);
view_stack_free(infrared->view_stack);

view_dispatcher_remove_view(view_dispatcher, InfraredViewMove);
infrared_move_view_free(infrared->move_view);

view_dispatcher_remove_view(view_dispatcher, InfraredViewLoading);
loading_free(infrared->loading);

widget_free(infrared->widget);
if(app_state->is_debug_enabled) {
view_dispatcher_remove_view(view_dispatcher, InfraredViewDebugView);
infrared_debug_view_free(infrared->debug_view);
}

button_panel_free(infrared->button_panel);
infrared_progress_view_free(infrared->progress);

// Free dispatcher
view_dispatcher_free(view_dispatcher);

// Free scene manager
scene_manager_free(infrared->scene_manager);

infrared_brute_force_free(infrared->brute_force);
infrared_signal_free(infrared->current_signal);
infrared_remote_free(infrared->remote);
// Free remaining views
button_panel_free(infrared->button_panel);
infrared_progress_view_free(infrared->progress);

// Free IR components
infrared_worker_free(infrared->worker);
infrared_remote_free(infrared->remote);
infrared_signal_free(infrared->current_signal);
infrared_brute_force_free(infrared->brute_force);

// Close system records with NULL assignments
furi_record_close(RECORD_NOTIFICATION);
infrared->notifications = NULL;

furi_record_close(RECORD_DIALOGS);
infrared->dialogs = NULL;

furi_record_close(RECORD_GUI);
infrared->gui = NULL;
furi_record_close(RECORD_STORAGE);
infrared->storage = NULL;

// Free strings
furi_string_free(infrared->file_path);
furi_string_free(infrared->button_name);

Expand Down
12 changes: 12 additions & 0 deletions applications/main/infrared/infrared_app_i.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
#include "views/infrared_debug_view.h"
#include "views/infrared_move_view.h"

#include <gui/modules/widget.h>

#define INFRARED_FILE_NAME_SIZE 100
#define INFRARED_TEXT_STORE_NUM 2
#define INFRARED_TEXT_STORE_SIZE 128
Expand Down Expand Up @@ -66,6 +68,12 @@ typedef enum {
InfraredEditTargetNone, /**< No editing target is selected. */
InfraredEditTargetRemote, /**< Whole remote is selected as editing target. */
InfraredEditTargetButton, /**< Single button is selected as editing target. */
InfraredEditTargetMetadataBrand,
InfraredEditTargetMetadataDeviceType,
InfraredEditTargetMetadataModel,
InfraredEditTargetMetadataContributor,
InfraredEditTargetMetadataRemoteModel,
InfraredEditTargetSignal,
} InfraredEditTarget;

/**
Expand All @@ -85,6 +93,8 @@ 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_contributing_remote; /**< Whether we're in the contribute flow */
bool is_processing_contribute_exit; /**< Guard flag for contribute exit */
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). */
Expand Down Expand Up @@ -132,6 +142,7 @@ struct InfraredApp {
InfraredAppState app_state; /**< Application state. */

void* rpc_ctx; /**< Pointer to the RPC context object. */
Widget* widget;
};

/**
Expand All @@ -148,6 +159,7 @@ typedef enum {
InfraredViewDebugView,
InfraredViewMove,
InfraredViewLoading,
InfraredViewWidget,
} InfraredView;

/**
Expand Down
238 changes: 238 additions & 0 deletions applications/main/infrared/infrared_metadata.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
#include "infrared_remote.h"
#include "infrared_metadata.h"
#include <toolbox/path.h>
#include <furi_hal_resources.h>
#include <toolbox/stream/stream.h>
#include <storage/storage.h>
#include <flipper_format/flipper_format.h>
#include <flipper_format/flipper_format_i.h>
#define TAG "InfraredMetadata"
// Metadata keys
#define INFRARED_METADATA_BRAND_KEY "Brand"
#define INFRARED_METADATA_DEVICE_TYPE_KEY "Device Type"
#define INFRARED_METADATA_MODEL_KEY "Model"
#define INFRARED_METADATA_CONTRIBUTOR_KEY "Contributor"
#define INFRARED_METADATA_REMOTE_MODEL_KEY "Remote Model"

struct InfraredMetadata {
FuriString* brand;
FuriString* device_type;
FuriString* model;
FuriString* contributor;
FuriString* remote_model;
};

InfraredMetadata* infrared_metadata_alloc() {
InfraredMetadata* metadata = malloc(sizeof(InfraredMetadata));
metadata->brand = furi_string_alloc();
metadata->device_type = furi_string_alloc();
metadata->model = furi_string_alloc();
metadata->contributor = furi_string_alloc();
metadata->remote_model = furi_string_alloc();
return metadata;
}

void infrared_metadata_free(InfraredMetadata* metadata) {
furi_assert(metadata);
furi_string_free(metadata->brand);
furi_string_free(metadata->device_type);
furi_string_free(metadata->model);
furi_string_free(metadata->contributor);
furi_string_free(metadata->remote_model);
free(metadata);
}

void infrared_metadata_reset(InfraredMetadata* metadata) {
furi_assert(metadata);
furi_string_reset(metadata->brand);
furi_string_reset(metadata->device_type);
furi_string_reset(metadata->model);
furi_string_reset(metadata->contributor);
furi_string_reset(metadata->remote_model);
}

InfraredErrorCode infrared_metadata_save(InfraredMetadata* metadata, FlipperFormat* ff) {
InfraredErrorCode error = InfraredErrorCodeNone;

FURI_LOG_D(
TAG,
"Saving metadata - Brand: '%s', Type: '%s', Model: '%s'",
furi_string_get_cstr(metadata->brand),
furi_string_get_cstr(metadata->device_type),
furi_string_get_cstr(metadata->model));

// Write blank line
if(!flipper_format_write_comment_cstr(ff, "")) {
FURI_LOG_E(TAG, "Failed to write blank line");
return InfraredErrorCodeFileOperationFailed;
}

// Write brand if exists
if(furi_string_size(metadata->brand) > 0) {
if(!flipper_format_write_string_cstr(
ff, "# Brand", furi_string_get_cstr(metadata->brand))) {
FURI_LOG_E(TAG, "Failed to write brand");
return InfraredErrorCodeFileOperationFailed;
}
}

// Write device type if exists
if(furi_string_size(metadata->device_type) > 0) {
if(!flipper_format_write_string_cstr(
ff, "# Device Type", furi_string_get_cstr(metadata->device_type))) {
FURI_LOG_E(TAG, "Failed to write device type");
return InfraredErrorCodeFileOperationFailed;
}
}

// Write model if exists
if(furi_string_size(metadata->model) > 0) {
if(!flipper_format_write_string_cstr(
ff, "# Model", furi_string_get_cstr(metadata->model))) {
FURI_LOG_E(TAG, "Failed to write model");
return InfraredErrorCodeFileOperationFailed;
}
}

// Write contributor if exists
if(furi_string_size(metadata->contributor) > 0) {
if(!flipper_format_write_string_cstr(
ff, "# Contributor", furi_string_get_cstr(metadata->contributor))) {
FURI_LOG_E(TAG, "Failed to write contributor");
return InfraredErrorCodeFileOperationFailed;
}
}

// Write remote model if exists
if(furi_string_size(metadata->remote_model) > 0) {
if(!flipper_format_write_string_cstr(
ff, "# Remote Model", furi_string_get_cstr(metadata->remote_model))) {
FURI_LOG_E(TAG, "Failed to write remote model");
return InfraredErrorCodeFileOperationFailed;
}
}

return error;
}

InfraredErrorCode infrared_metadata_read(InfraredMetadata* metadata, FlipperFormat* ff) {
infrared_metadata_reset(metadata);
InfraredErrorCode error = InfraredErrorCodeNone;
FuriString* line = furi_string_alloc();

FURI_LOG_D(TAG, "Starting metadata read");

Stream* stream = flipper_format_get_raw_stream(ff);
if(!stream) {
FURI_LOG_E(TAG, "Failed to get stream");
furi_string_free(line);
return InfraredErrorCodeFileOperationFailed;
}

// Store current position
size_t pos = stream_tell(stream);

do {
// Rewind and skip header
if(!stream_rewind(stream)) {
FURI_LOG_E(TAG, "Failed to rewind stream");
error = InfraredErrorCodeFileOperationFailed;
break;
}

// Skip header (first two lines)
for(int i = 0; i < 2; i++) {
if(!stream_read_line(stream, line)) {
FURI_LOG_E(TAG, "Failed to skip header line %d", i + 1);
error = InfraredErrorCodeFileOperationFailed;
break;
}
}
if(error != InfraredErrorCodeNone) break;

FURI_LOG_D(TAG, "Reading metadata comments");

// Read lines until we find a non-comment
while(!stream_eof(stream)) {
if(!stream_read_line(stream, line)) break;
const char* line_str = furi_string_get_cstr(line);

// Skip non-comment or empty comment lines
if(strncmp(line_str, "# ", 2) != 0 || strlen(line_str) <= 2) continue;

// Parse "# Key: Value" format
const char* content = line_str + 2;
char* sep = strstr(content, ": ");
if(!sep) continue;

size_t key_len = sep - content;
char* value = sep + 2;

if(strncmp(content, "Brand", key_len) == 0) {
furi_string_set(metadata->brand, value);
FURI_LOG_D(TAG, "Found brand: '%s'", value);
} else if(strncmp(content, "Device Type", key_len) == 0) {
furi_string_set(metadata->device_type, value);
FURI_LOG_D(TAG, "Found device type: '%s'", value);
} else if(strncmp(content, "Model", key_len) == 0) {
furi_string_set(metadata->model, value);
FURI_LOG_D(TAG, "Found model: '%s'", value);
} else if(strncmp(content, "Contributor", key_len) == 0) {
furi_string_set(metadata->contributor, value);
FURI_LOG_D(TAG, "Found contributor: '%s'", value);
} else if(strncmp(content, "Remote Model", key_len) == 0) {
furi_string_set(metadata->remote_model, value);
FURI_LOG_D(TAG, "Found remote model: '%s'", value);
}
}

} while(false);

// Always try to restore position
if(!stream_seek(stream, pos, StreamOffsetFromStart)) {
FURI_LOG_E(TAG, "Failed to restore stream position");
error = InfraredErrorCodeFileOperationFailed;
}

furi_string_free(line);
return error;
}
const char* infrared_metadata_get_brand(const InfraredMetadata* metadata) {
return furi_string_get_cstr(metadata->brand);
}

const char* infrared_metadata_get_device_type(const InfraredMetadata* metadata) {
return furi_string_get_cstr(metadata->device_type);
}

const char* infrared_metadata_get_model(const InfraredMetadata* metadata) {
return furi_string_get_cstr(metadata->model);
}

void infrared_metadata_set_brand(InfraredMetadata* metadata, const char* brand) {
furi_string_set(metadata->brand, brand);
}

void infrared_metadata_set_device_type(InfraredMetadata* metadata, const char* device_type) {
furi_string_set(metadata->device_type, device_type);
}

void infrared_metadata_set_model(InfraredMetadata* metadata, const char* model) {
furi_string_set(metadata->model, model);
}

const char* infrared_metadata_get_contributor(const InfraredMetadata* metadata) {
return furi_string_get_cstr(metadata->contributor);
}

const char* infrared_metadata_get_remote_model(const InfraredMetadata* metadata) {
return furi_string_get_cstr(metadata->remote_model);
}

void infrared_metadata_set_contributor(InfraredMetadata* metadata, const char* contributor) {
furi_string_set(metadata->contributor, contributor);
}

void infrared_metadata_set_remote_model(InfraredMetadata* metadata, const char* remote_model) {
furi_string_set(metadata->remote_model, remote_model);
}
Loading