From 7c26ff30752d1e9983cdb58c8e3a5b320f9bbda5 Mon Sep 17 00:00:00 2001 From: RocketGod <57732082+RocketGod-git@users.noreply.github.com> Date: Sat, 20 Dec 2025 14:00:42 -0800 Subject: [PATCH] Add Sub Decode menu option and scene for .sub protocol file analysis Introduces a new 'Sub Decode' scene to the ProtoPirate app, allowing users to select and analyze SubGhz protocol files. The scene supports both RAW and protocol-based decoding, provides animated feedback for success or failure, and displays detailed results or error information. Updates include menu integration, scene registration, and all necessary logic for file handling, decoding, and UI rendering. --- dist/proto_pirate.fap | Bin 81004 -> 90660 bytes protopirate_app_i.h | 1 + scenes/protopirate_scene_config.h | 1 + scenes/protopirate_scene_start.c | 16 +- scenes/protopirate_scene_sub_decode.c | 886 ++++++++++++++++++++++++++ 5 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 scenes/protopirate_scene_sub_decode.c diff --git a/dist/proto_pirate.fap b/dist/proto_pirate.fap index 447efe127ab11c1e2116bb9a6654313f6e40ee87..bfad7850ec1572ea3b138b880613a43ab63a85d7 100644 GIT binary patch delta 32642 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 Jb7yJ&|36rk)l&cf delta 23370 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 diff --git a/protopirate_app_i.h b/protopirate_app_i.h index 29e35ea..ca2b4ee 100644 --- a/protopirate_app_i.h +++ b/protopirate_app_i.h @@ -20,6 +20,7 @@ #include #include #include +#include typedef struct ProtoPirateApp ProtoPirateApp; diff --git a/scenes/protopirate_scene_config.h b/scenes/protopirate_scene_config.h index 7e4f9a0..1d614f7 100644 --- a/scenes/protopirate_scene_config.h +++ b/scenes/protopirate_scene_config.h @@ -1,5 +1,6 @@ // scenes/protopirate_scene_config.h ADD_SCENE(protopirate, start, Start) +ADD_SCENE(protopirate, sub_decode, SubDecode) ADD_SCENE(protopirate, about, About) ADD_SCENE(protopirate, receiver, Receiver) ADD_SCENE(protopirate, receiver_config, ReceiverConfig) diff --git a/scenes/protopirate_scene_start.c b/scenes/protopirate_scene_start.c index fc709b3..1f4e170 100644 --- a/scenes/protopirate_scene_start.c +++ b/scenes/protopirate_scene_start.c @@ -6,6 +6,7 @@ typedef enum SubmenuIndexProtoPirateReceiver, SubmenuIndexProtoPirateSaved, SubmenuIndexProtoPirateReceiverConfig, + SubmenuIndexProtoPirateSubDecode, SubmenuIndexProtoPirateAbout, } SubmenuIndex; @@ -42,6 +43,14 @@ void protopirate_scene_start_on_enter(void *context) protopirate_scene_start_submenu_callback, app); + // ADD THIS ITEM + submenu_add_item( + app->submenu, + "Sub Decode", + SubmenuIndexProtoPirateSubDecode, + protopirate_scene_start_submenu_callback, + app); + submenu_add_item( app->submenu, "About", @@ -83,6 +92,11 @@ bool protopirate_scene_start_on_event(void *context, SceneManagerEvent event) scene_manager_next_scene(app->scene_manager, ProtoPirateSceneReceiverConfig); consumed = true; } + else if (event.event == SubmenuIndexProtoPirateSubDecode) + { + scene_manager_next_scene(app->scene_manager, ProtoPirateSceneSubDecode); + consumed = true; + } scene_manager_set_scene_state(app->scene_manager, ProtoPirateSceneStart, event.event); } @@ -94,4 +108,4 @@ void protopirate_scene_start_on_exit(void *context) furi_assert(context); ProtoPirateApp *app = context; submenu_reset(app->submenu); -} +} \ No newline at end of file diff --git a/scenes/protopirate_scene_sub_decode.c b/scenes/protopirate_scene_sub_decode.c new file mode 100644 index 0000000..84e771b --- /dev/null +++ b/scenes/protopirate_scene_sub_decode.c @@ -0,0 +1,886 @@ +// scenes/protopirate_scene_sub_decode.c +#include "../protopirate_app_i.h" +#include "../protocols/protocol_items.h" +#include +#include +#include + +#define TAG "ProtoPirateSubDecode" + +#define SUBGHZ_APP_FOLDER EXT_PATH("subghz") +#define SAMPLES_PER_TICK 256 +#define MAX_RAW_SAMPLES 8192 +#define SUCCESS_DISPLAY_TICKS 18 +#define FAILURE_DISPLAY_TICKS 18 + +// Decode state machine +typedef enum { + DecodeStateIdle, + DecodeStateOpenFile, + DecodeStateReadHeader, + DecodeStateLoadRawSamples, + DecodeStateDecodingRaw, + DecodeStateDecodingProtocol, + DecodeStateShowSuccess, + DecodeStateShowFailure, + DecodeStateDone, +} DecodeState; + +// Context for the whole decode operation +typedef struct { + DecodeState state; + uint16_t animation_frame; + uint8_t result_display_counter; + + // File info + FuriString* file_path; + FuriString* protocol_name; + FuriString* result; + FuriString* error_info; + uint32_t frequency; + + // File handle + Storage* storage; + FlipperFormat* ff; + + // RAW decode state + int32_t* raw_samples; + size_t total_samples; + size_t current_sample; + size_t current_protocol_idx; + void* current_decoder; + const SubGhzProtocol* current_protocol; + bool decode_success; + + // Callback context + bool callback_fired; + FuriString* decoded_string; +} SubDecodeContext; + +static SubDecodeContext* g_decode_ctx = NULL; + +// Callback when decoder successfully decodes +static void protopirate_decode_callback(SubGhzProtocolDecoderBase* decoder_base, void* context) { + SubDecodeContext* ctx = context; + ctx->callback_fired = true; + + if(ctx->current_protocol && ctx->current_protocol->decoder && ctx->current_protocol->decoder->get_string) { + ctx->current_protocol->decoder->get_string(decoder_base, ctx->decoded_string); + } + + FURI_LOG_I(TAG, "Decode callback fired for %s!", ctx->current_protocol->name); +} + +// Case-insensitive string search +static bool str_contains_ci(const char* haystack, const char* needle) { + if(!haystack || !needle) return false; + + size_t haystack_len = strlen(haystack); + size_t needle_len = strlen(needle); + + if(needle_len > haystack_len) return false; + + for(size_t i = 0; i <= haystack_len - needle_len; i++) { + bool match = true; + for(size_t j = 0; j < needle_len; j++) { + if(tolower((unsigned char)haystack[i + j]) != tolower((unsigned char)needle[j])) { + match = false; + break; + } + } + if(match) return true; + } + return false; +} + +// Check if protocol names match +static bool protocol_names_match(const char* file_proto, const char* registry_proto) { + if(!file_proto || !registry_proto) return false; + + if(strcasecmp(file_proto, registry_proto) == 0) return true; + if(str_contains_ci(file_proto, registry_proto)) return true; + + const char* file_version = NULL; + const char* reg_version = NULL; + + for(const char* p = file_proto; *p; p++) { + if((*p == 'V' || *p == 'v') && isdigit((unsigned char)*(p + 1))) { + file_version = p; + break; + } + } + + for(const char* p = registry_proto; *p; p++) { + if((*p == 'V' || *p == 'v') && isdigit((unsigned char)*(p + 1))) { + reg_version = p; + break; + } + } + + if(file_version && reg_version) { + size_t file_ver_len = 0; + size_t reg_ver_len = 0; + + for(const char* p = file_version; *p && (isalnum((unsigned char)*p) || *p == '/'); p++) { + file_ver_len++; + } + for(const char* p = reg_version; *p && (isalnum((unsigned char)*p) || *p == '/'); p++) { + reg_ver_len++; + } + + if(file_ver_len == reg_ver_len && + strncasecmp(file_version, reg_version, file_ver_len) == 0) { + if((str_contains_ci(file_proto, "KIA") || str_contains_ci(file_proto, "HYU")) && + str_contains_ci(registry_proto, "Kia")) { + return true; + } + if(str_contains_ci(file_proto, "Ford") && str_contains_ci(registry_proto, "Ford")) { + return true; + } + if(str_contains_ci(file_proto, "Subaru") && str_contains_ci(registry_proto, "Subaru")) { + return true; + } + if(str_contains_ci(file_proto, "Suzuki") && str_contains_ci(registry_proto, "Suzuki")) { + return true; + } + if(str_contains_ci(file_proto, "VW") && str_contains_ci(registry_proto, "VW")) { + return true; + } + } + } + + return false; +} + +// Get human readable error string from status +static const char* get_protocol_status_string(SubGhzProtocolStatus status) { + switch(status) { + case SubGhzProtocolStatusOk: return "OK"; + case SubGhzProtocolStatusErrorParserHeader: return "Header parse error"; + case SubGhzProtocolStatusErrorParserFrequency: return "Frequency error"; + case SubGhzProtocolStatusErrorParserPreset: return "Preset error"; + case SubGhzProtocolStatusErrorParserCustomPreset: return "Custom preset error"; + case SubGhzProtocolStatusErrorParserProtocolName: return "Protocol name error"; + case SubGhzProtocolStatusErrorParserOthers: return "Parse error"; + case SubGhzProtocolStatusErrorValueBitCount: return "Bit count mismatch"; + case SubGhzProtocolStatusErrorParserKey: return "Key parse error"; + case SubGhzProtocolStatusErrorParserTe: return "TE parse error"; + default: return "Unknown error"; + } +} + +// Draw the decoding animation +static void protopirate_decode_draw_callback(Canvas* canvas, void* context) { + UNUSED(context); + SubDecodeContext* ctx = g_decode_ctx; + if(!ctx) return; + + canvas_clear(canvas); + + if(ctx->state == DecodeStateIdle || ctx->state == DecodeStateDone) { + return; + } + + canvas_set_color(canvas, ColorBlack); + uint16_t frame = ctx->animation_frame; + + // Check for success/failure display states + if(ctx->state == DecodeStateShowSuccess) { + // Success screen + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 64, 6, AlignCenter, AlignTop, "DECODED!"); + + // Checkmark animation + int check_progress = ctx->result_display_counter * 3; + int cx = 64, cy = 32; + int size = 12; + + // First stroke of check (going down-right from left) + int stroke1_max = size; + int stroke1_len = (check_progress > stroke1_max) ? stroke1_max : check_progress; + for(int i = 0; i <= stroke1_len; i++) { + canvas_draw_dot(canvas, cx - size + i, cy - size/2 + i); + canvas_draw_dot(canvas, cx - size + i, cy - size/2 + i + 1); + canvas_draw_dot(canvas, cx - size + i + 1, cy - size/2 + i); + } + + // Second stroke of check (going up-right) + if(check_progress > stroke1_max) { + int stroke2_max = size * 2; + int stroke2_len = check_progress - stroke1_max; + if(stroke2_len > stroke2_max) stroke2_len = stroke2_max; + for(int i = 0; i <= stroke2_len; i++) { + canvas_draw_dot(canvas, cx + i, cy + size/2 - i); + canvas_draw_dot(canvas, cx + i, cy + size/2 - i - 1); + canvas_draw_dot(canvas, cx + i + 1, cy + size/2 - i); + } + } + + // Radiating dots + for(int r = 0; r < 3; r++) { + int radius = ((frame * 2 + r * 12) % 35) + 8; + if(radius < 30) { + for(int angle = 0; angle < 12; angle++) { + float a = (float)angle * 3.14159f * 2.0f / 12.0f; + int x = cx + (int)(radius * cosf(a)); + int y = cy + (int)(radius * sinf(a)); + if(x >= 0 && x < 128 && y >= 0 && y < 64) { + canvas_draw_dot(canvas, x, y); + } + } + } + } + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 64, 54, AlignCenter, AlignTop, "Signal matched!"); + return; + } + + if(ctx->state == DecodeStateShowFailure) { + // Failure screen + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 64, 6, AlignCenter, AlignTop, "NO MATCH"); + + // X animation + int x_progress = ctx->result_display_counter * 3; + int cx = 64, cy = 32; + int size = 10; + int stroke_len = size * 2 + 1; // Full diagonal length + + // First stroke: top-left to bottom-right + int stroke1_len = (x_progress > stroke_len) ? stroke_len : x_progress; + for(int i = 0; i < stroke1_len; i++) { + int x = cx - size + i; + int y = cy - size + i; + canvas_draw_dot(canvas, x, y); + canvas_draw_dot(canvas, x + 1, y); + canvas_draw_dot(canvas, x, y + 1); + } + + // Second stroke: top-right to bottom-left + if(x_progress > stroke_len) { + int stroke2_progress = x_progress - stroke_len; + int stroke2_len = (stroke2_progress > stroke_len) ? stroke_len : stroke2_progress; + for(int i = 0; i < stroke2_len; i++) { + int x = cx + size - i; + int y = cy - size + i; + canvas_draw_dot(canvas, x, y); + canvas_draw_dot(canvas, x - 1, y); + canvas_draw_dot(canvas, x, y + 1); + } + } + + // Static noise effect around the edges + for(int i = 0; i < 30; i++) { + int x = ((frame * 7 + i * 17) * 31) % 128; + int y = ((frame * 13 + i * 23) * 17) % 64; + canvas_draw_dot(canvas, x, y); + } + + canvas_set_font(canvas, FontSecondary); + // Show error info if we have it + if(furi_string_size(ctx->error_info) > 0) { + canvas_draw_str_aligned(canvas, 64, 54, AlignCenter, AlignTop, + furi_string_get_cstr(ctx->error_info)); + } else { + canvas_draw_str_aligned(canvas, 64, 54, AlignCenter, AlignTop, "Unknown protocol"); + } + return; + } + + // Normal decoding animation + + // Title with occasional glitch + canvas_set_font(canvas, FontPrimary); + int glitch = (frame % 47 == 0) ? 1 : 0; + canvas_draw_str_aligned(canvas, 64 + glitch, 0, AlignCenter, AlignTop, "DECODING"); + + // Waveform visualization - original style with sinf + int wave_y = 22; + int wave_height = 14; + + for(int x = 0; x < 128; x++) { + float phase = (float)(x + frame * 4) * 0.12f; + float phase2 = (float)(x - frame * 2) * 0.08f; + int y_offset = (int)(sinf(phase) * wave_height / 2 + sinf(phase2) * wave_height / 4); + + // Add some noise variation + if((x * 7 + frame) % 13 == 0) { + y_offset += ((frame * x) % 5) - 2; + } + + canvas_draw_dot(canvas, x, wave_y + y_offset); + + // Thicker line + if((x + frame) % 3 != 0) { + canvas_draw_dot(canvas, x, wave_y + y_offset + 1); + } + } + + // Scanning beam effect + int scan_x = (frame * 5) % 148 - 10; + for(int dx = 0; dx < 8; dx++) { + int sx = scan_x + dx; + if(sx >= 0 && sx < 128) { + int intensity = 8 - dx; + for(int y = wave_y - wave_height/2 - 1; y <= wave_y + wave_height/2 + 1; y++) { + if(dx < intensity / 2) { + canvas_draw_dot(canvas, sx, y); + } + } + } + } + + // Progress bar frame + int progress_y = 38; + canvas_draw_rframe(canvas, 8, progress_y, 112, 10, 2); + + // Calculate progress + int progress = 0; + if(ctx->state == DecodeStateLoadRawSamples && ctx->total_samples > 0) { + progress = 10 + (ctx->total_samples * 20) / MAX_RAW_SAMPLES; + } else if(ctx->state == DecodeStateDecodingRaw && ctx->total_samples > 0) { + int sample_pct = (ctx->current_sample * 100) / ctx->total_samples; + int proto_pct = (ctx->current_protocol_idx * 100) / protopirate_protocol_registry.size; + progress = 30 + (sample_pct * 35 + proto_pct * 35) / 100; + } else if(ctx->state == DecodeStateOpenFile || ctx->state == DecodeStateReadHeader) { + progress = 5 + (frame % 10); + } else if(ctx->state == DecodeStateDecodingProtocol) { + progress = 50 + (frame % 30); + } + if(progress > 100) progress = 100; + + // Animated progress fill with diagonal stripes + int fill_width = (progress * 108) / 100; + for(int x = 0; x < fill_width; x++) { + for(int y = 0; y < 6; y++) { + if(((x - (int)frame + y) & 3) < 2) { + canvas_draw_dot(canvas, 10 + x, progress_y + 2 + y); + } + } + } + + // Status text + canvas_set_font(canvas, FontSecondary); + const char* status_text = "Starting..."; + + switch(ctx->state) { + case DecodeStateOpenFile: + status_text = "Opening file..."; + break; + case DecodeStateReadHeader: + status_text = "Reading header..."; + break; + case DecodeStateLoadRawSamples: + status_text = "Loading samples..."; + break; + case DecodeStateDecodingRaw: + status_text = ctx->current_protocol ? ctx->current_protocol->name : "Analyzing..."; + break; + case DecodeStateDecodingProtocol: + status_text = "Parsing protocol..."; + break; + default: + break; + } + canvas_draw_str_aligned(canvas, 64, 52, AlignCenter, AlignTop, status_text); + + // Binary rain effect on sides + canvas_set_font(canvas, FontKeyboard); + for(int i = 0; i < 5; i++) { + int y_left = ((frame * 2 + i * 13) % 70) - 5; + int y_right = ((frame * 2 + i * 17 + 35) % 70) - 5; + char bit_l = '0' + ((frame + i) & 1); + char bit_r = '0' + ((frame + i + 1) & 1); + char str_l[2] = {bit_l, 0}; + char str_r[2] = {bit_r, 0}; + + if(y_left >= 0 && y_left < 64) canvas_draw_str(canvas, 1, y_left, str_l); + if(y_right >= 0 && y_right < 64) canvas_draw_str(canvas, 123, y_right, str_r); + } + + // Corner spinners (slower) + const char* spin = "|/-\\"; + char spinner[2] = {spin[(frame / 4) & 3], 0}; + canvas_draw_str(canvas, 1, 62, spinner); + canvas_draw_str(canvas, 123, 62, spinner); +} + +static bool protopirate_decode_input_callback(InputEvent* event, void* context) { + UNUSED(context); + + if(event->type == InputTypeShort && event->key == InputKeyBack) { + if(g_decode_ctx && g_decode_ctx->state != DecodeStateIdle && + g_decode_ctx->state != DecodeStateDone) { + furi_string_set(g_decode_ctx->error_info, "Cancelled"); + g_decode_ctx->state = DecodeStateShowFailure; + g_decode_ctx->result_display_counter = 0; + furi_string_set(g_decode_ctx->result, "Cancelled by user"); + } + return true; + } + + return false; +} + +// Process one chunk of RAW samples +static bool protopirate_process_raw_chunk(ProtoPirateApp* app, SubDecodeContext* ctx) { + if(!ctx->current_decoder) { + while(ctx->current_protocol_idx < protopirate_protocol_registry.size) { + const SubGhzProtocol* protocol = protopirate_protocol_registry.items[ctx->current_protocol_idx]; + + if(protocol->decoder && protocol->decoder->alloc) { + ctx->current_decoder = protocol->decoder->alloc(app->txrx->environment); + ctx->current_protocol = protocol; + ctx->current_sample = 0; + ctx->callback_fired = false; + furi_string_reset(ctx->decoded_string); + + if(ctx->current_decoder) { + SubGhzProtocolDecoderBase* decoder_base = ctx->current_decoder; + decoder_base->callback = protopirate_decode_callback; + decoder_base->context = ctx; + + if(protocol->decoder->reset) { + protocol->decoder->reset(ctx->current_decoder); + } + + FURI_LOG_D(TAG, "Trying protocol: %s", protocol->name); + break; + } + } + ctx->current_protocol_idx++; + } + + if(!ctx->current_decoder) { + return true; + } + } + + size_t end_sample = ctx->current_sample + SAMPLES_PER_TICK; + if(end_sample > ctx->total_samples) { + end_sample = ctx->total_samples; + } + + for(size_t i = ctx->current_sample; i < end_sample && !ctx->callback_fired; i++) { + int32_t duration = ctx->raw_samples[i]; + bool level = (duration >= 0); + if(duration < 0) duration = -duration; + + ctx->current_protocol->decoder->feed(ctx->current_decoder, level, (uint32_t)duration); + } + + ctx->current_sample = end_sample; + + if(ctx->callback_fired && furi_string_size(ctx->decoded_string) > 0) { + furi_string_printf(ctx->result, "RAW Decoded!\nFreq: %lu.%02lu MHz\n\n%s", + ctx->frequency / 1000000, + (ctx->frequency % 1000000) / 10000, + furi_string_get_cstr(ctx->decoded_string)); + ctx->decode_success = true; + + ctx->current_protocol->decoder->free(ctx->current_decoder); + ctx->current_decoder = NULL; + return true; + } + + if(ctx->current_sample >= ctx->total_samples) { + if(ctx->current_decoder) { + ctx->current_protocol->decoder->free(ctx->current_decoder); + ctx->current_decoder = NULL; + } + ctx->current_protocol_idx++; + ctx->current_sample = 0; + + if(ctx->current_protocol_idx >= protopirate_protocol_registry.size) { + return true; + } + } + + return false; +} + +static void close_file_handles(SubDecodeContext* ctx) { + if(ctx->ff) { + flipper_format_free(ctx->ff); + ctx->ff = NULL; + } + if(ctx->storage) { + furi_record_close(RECORD_STORAGE); + ctx->storage = NULL; + } +} + +void protopirate_scene_sub_decode_on_enter(void* context) { + ProtoPirateApp* app = context; + + g_decode_ctx = malloc(sizeof(SubDecodeContext)); + memset(g_decode_ctx, 0, sizeof(SubDecodeContext)); + g_decode_ctx->file_path = furi_string_alloc(); + g_decode_ctx->protocol_name = furi_string_alloc(); + g_decode_ctx->result = furi_string_alloc(); + g_decode_ctx->error_info = furi_string_alloc(); + g_decode_ctx->decoded_string = furi_string_alloc(); + g_decode_ctx->state = DecodeStateIdle; + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, ".sub", NULL); + browser_options.base_path = SUBGHZ_APP_FOLDER; + browser_options.hide_ext = false; + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + furi_string_set(g_decode_ctx->file_path, SUBGHZ_APP_FOLDER); + + if(dialog_file_browser_show(dialogs, g_decode_ctx->file_path, g_decode_ctx->file_path, &browser_options)) { + FURI_LOG_I(TAG, "Selected file: %s", furi_string_get_cstr(g_decode_ctx->file_path)); + g_decode_ctx->state = DecodeStateOpenFile; + + view_set_draw_callback(app->view_about, protopirate_decode_draw_callback); + view_set_input_callback(app->view_about, protopirate_decode_input_callback); + view_set_context(app->view_about, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewAbout); + } else { + scene_manager_previous_scene(app->scene_manager); + } + + furi_record_close(RECORD_DIALOGS); +} + +bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent event) { + ProtoPirateApp* app = context; + bool consumed = false; + SubDecodeContext* ctx = g_decode_ctx; + + if(!ctx) return false; + + if(event.type == SceneManagerEventTypeTick) { + consumed = true; + ctx->animation_frame++; + + switch(ctx->state) { + case DecodeStateOpenFile: { + ctx->storage = furi_record_open(RECORD_STORAGE); + ctx->ff = flipper_format_file_alloc(ctx->storage); + + if(!flipper_format_file_open_existing(ctx->ff, furi_string_get_cstr(ctx->file_path))) { + furi_string_set(ctx->result, "Failed to open file"); + furi_string_set(ctx->error_info, "File open failed"); + close_file_handles(ctx); + ctx->state = DecodeStateShowFailure; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_error); + } else { + ctx->state = DecodeStateReadHeader; + } + break; + } + + case DecodeStateReadHeader: { + FuriString* temp_str = furi_string_alloc(); + uint32_t version = 0; + bool success = false; + + do { + if(!flipper_format_read_header(ctx->ff, temp_str, &version)) { + furi_string_set(ctx->result, "Invalid file format"); + furi_string_set(ctx->error_info, "Invalid header"); + break; + } + + if(furi_string_cmp_str(temp_str, "Flipper SubGhz Key File") != 0 && + furi_string_cmp_str(temp_str, "Flipper SubGhz RAW File") != 0 && + furi_string_cmp_str(temp_str, "Flipper SubGhz") != 0) { + furi_string_set(ctx->result, "Not a SubGhz file"); + furi_string_set(ctx->error_info, "Not SubGhz file"); + break; + } + + if(!flipper_format_read_string(ctx->ff, "Protocol", ctx->protocol_name)) { + furi_string_set(ctx->result, "Missing Protocol"); + furi_string_set(ctx->error_info, "No protocol field"); + break; + } + + flipper_format_rewind(ctx->ff); + flipper_format_read_header(ctx->ff, temp_str, &version); + ctx->frequency = 433920000; + flipper_format_read_uint32(ctx->ff, "Frequency", &ctx->frequency, 1); + + FURI_LOG_I(TAG, "Protocol: %s, Freq: %lu", + furi_string_get_cstr(ctx->protocol_name), ctx->frequency); + + success = true; + } while(false); + + furi_string_free(temp_str); + + if(!success) { + close_file_handles(ctx); + ctx->state = DecodeStateShowFailure; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_error); + } else if(furi_string_cmp_str(ctx->protocol_name, "RAW") == 0) { + ctx->raw_samples = malloc(sizeof(int32_t) * MAX_RAW_SAMPLES); + if(!ctx->raw_samples) { + furi_string_set(ctx->result, "Memory error"); + furi_string_set(ctx->error_info, "Out of memory"); + close_file_handles(ctx); + ctx->state = DecodeStateShowFailure; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_error); + } else { + ctx->total_samples = 0; + flipper_format_rewind(ctx->ff); + ctx->state = DecodeStateLoadRawSamples; + } + } else { + ctx->state = DecodeStateDecodingProtocol; + } + break; + } + + case DecodeStateLoadRawSamples: { + size_t samples_this_tick = 0; + + while(ctx->total_samples < MAX_RAW_SAMPLES && samples_this_tick < 512) { + uint32_t count = 0; + if(!flipper_format_get_value_count(ctx->ff, "RAW_Data", &count) || count == 0) { + break; + } + + size_t to_read = count; + if(ctx->total_samples + to_read > MAX_RAW_SAMPLES) { + to_read = MAX_RAW_SAMPLES - ctx->total_samples; + } + + if(!flipper_format_read_int32(ctx->ff, "RAW_Data", &ctx->raw_samples[ctx->total_samples], to_read)) { + break; + } + + ctx->total_samples += to_read; + samples_this_tick += to_read; + } + + uint32_t count = 0; + bool more_data = flipper_format_get_value_count(ctx->ff, "RAW_Data", &count) && count > 0; + + if(!more_data || ctx->total_samples >= MAX_RAW_SAMPLES) { + close_file_handles(ctx); + + FURI_LOG_I(TAG, "Loaded %zu RAW samples", ctx->total_samples); + + if(ctx->total_samples < 10) { + furi_string_set(ctx->result, "Not enough samples"); + furi_string_set(ctx->error_info, "Too few samples"); + ctx->state = DecodeStateShowFailure; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_error); + } else { + ctx->current_protocol_idx = 0; + ctx->current_sample = 0; + ctx->state = DecodeStateDecodingRaw; + } + } + break; + } + + case DecodeStateDecodingRaw: { + bool done = protopirate_process_raw_chunk(app, ctx); + + if(done) { + if(ctx->decode_success) { + ctx->state = DecodeStateShowSuccess; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_success); + } else { + furi_string_printf(ctx->result, + "RAW Signal\n\n" + "Freq: %lu.%02lu MHz\n" + "Samples: %zu\n\n" + "No ProtoPirate protocol\n" + "detected in signal.", + ctx->frequency / 1000000, + (ctx->frequency % 1000000) / 10000, + ctx->total_samples); + furi_string_set(ctx->error_info, "No protocol match"); + ctx->state = DecodeStateShowFailure; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_error); + } + } + break; + } + + case DecodeStateDecodingProtocol: { + const char* proto_name = furi_string_get_cstr(ctx->protocol_name); + bool decoded = false; + SubGhzProtocolStatus last_status = SubGhzProtocolStatusOk; + + // Find matching protocol + const SubGhzProtocol* custom_protocol = NULL; + for(size_t i = 0; i < protopirate_protocol_registry.size; i++) { + if(protocol_names_match(proto_name, protopirate_protocol_registry.items[i]->name)) { + custom_protocol = protopirate_protocol_registry.items[i]; + FURI_LOG_I(TAG, "Matched to: %s", custom_protocol->name); + break; + } + } + + if(custom_protocol && custom_protocol->decoder && custom_protocol->decoder->alloc) { + void* decoder = custom_protocol->decoder->alloc(app->txrx->environment); + if(decoder) { + flipper_format_rewind(ctx->ff); + last_status = custom_protocol->decoder->deserialize(decoder, ctx->ff); + + if(last_status == SubGhzProtocolStatusOk) { + FuriString* dec_str = furi_string_alloc(); + custom_protocol->decoder->get_string(decoder, dec_str); + + const char* fname = furi_string_get_cstr(ctx->file_path); + const char* short_name = strrchr(fname, '/'); + if(short_name) short_name++; else short_name = fname; + + furi_string_printf(ctx->result, "File: %s\n\n%s", + short_name, furi_string_get_cstr(dec_str)); + furi_string_free(dec_str); + decoded = true; + ctx->decode_success = true; + } else { + FURI_LOG_W(TAG, "Custom decoder failed: %d", last_status); + } + custom_protocol->decoder->free(decoder); + } + } + + if(!decoded) { + SubGhzProtocolDecoderBase* decoder = subghz_receiver_search_decoder_base_by_name( + app->txrx->receiver, proto_name); + + if(decoder) { + flipper_format_rewind(ctx->ff); + last_status = subghz_protocol_decoder_base_deserialize(decoder, ctx->ff); + + if(last_status == SubGhzProtocolStatusOk) { + FuriString* dec_str = furi_string_alloc(); + subghz_protocol_decoder_base_get_string(decoder, dec_str); + + const char* fname = furi_string_get_cstr(ctx->file_path); + const char* short_name = strrchr(fname, '/'); + if(short_name) short_name++; else short_name = fname; + + furi_string_printf(ctx->result, "File: %s\n\n%s", + short_name, furi_string_get_cstr(dec_str)); + furi_string_free(dec_str); + decoded = true; + ctx->decode_success = true; + } else { + FURI_LOG_W(TAG, "App receiver failed: %d", last_status); + } + } + } + + if(!decoded) { + const char* fname = furi_string_get_cstr(ctx->file_path); + const char* short_name = strrchr(fname, '/'); + if(short_name) short_name++; else short_name = fname; + + // Set error info based on status + furi_string_set(ctx->error_info, get_protocol_status_string(last_status)); + + furi_string_printf(ctx->result, "File: %s\nProtocol: %s\n\nError: %s\n\n", + short_name, proto_name, get_protocol_status_string(last_status)); + + // Read available fields to show what we can + FuriString* temp = furi_string_alloc(); + uint32_t version, val; + + flipper_format_rewind(ctx->ff); + flipper_format_read_header(ctx->ff, temp, &version); + if(flipper_format_read_uint32(ctx->ff, "Bit", &val, 1)) { + furi_string_cat_printf(ctx->result, "Bits: %lu\n", val); + } + + flipper_format_rewind(ctx->ff); + flipper_format_read_header(ctx->ff, temp, &version); + if(flipper_format_read_string(ctx->ff, "Key", temp)) { + furi_string_cat_printf(ctx->result, "Key: %s\n", furi_string_get_cstr(temp)); + } + + furi_string_cat_printf(ctx->result, "Freq: %lu.%02lu MHz\n", + ctx->frequency / 1000000, (ctx->frequency % 1000000) / 10000); + + furi_string_free(temp); + } + + close_file_handles(ctx); + + if(ctx->decode_success) { + ctx->state = DecodeStateShowSuccess; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_success); + } else { + ctx->state = DecodeStateShowFailure; + ctx->result_display_counter = 0; + notification_message(app->notifications, &sequence_error); + } + break; + } + + case DecodeStateShowSuccess: { + ctx->result_display_counter++; + if(ctx->result_display_counter >= SUCCESS_DISPLAY_TICKS) { + widget_reset(app->widget); + widget_add_text_scroll_element(app->widget, 0, 0, 128, 64, furi_string_get_cstr(ctx->result)); + view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget); + ctx->state = DecodeStateDone; + } + break; + } + + case DecodeStateShowFailure: { + ctx->result_display_counter++; + if(ctx->result_display_counter >= FAILURE_DISPLAY_TICKS) { + widget_reset(app->widget); + widget_add_text_scroll_element(app->widget, 0, 0, 128, 64, furi_string_get_cstr(ctx->result)); + view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget); + ctx->state = DecodeStateDone; + } + break; + } + + default: + break; + } + + view_commit_model(app->view_about, false); + } + + return consumed; +} + +void protopirate_scene_sub_decode_on_exit(void* context) { + ProtoPirateApp* app = context; + + if(g_decode_ctx) { + close_file_handles(g_decode_ctx); + + if(g_decode_ctx->current_decoder && g_decode_ctx->current_protocol) { + g_decode_ctx->current_protocol->decoder->free(g_decode_ctx->current_decoder); + } + if(g_decode_ctx->raw_samples) { + free(g_decode_ctx->raw_samples); + } + furi_string_free(g_decode_ctx->file_path); + furi_string_free(g_decode_ctx->protocol_name); + furi_string_free(g_decode_ctx->result); + furi_string_free(g_decode_ctx->error_info); + furi_string_free(g_decode_ctx->decoded_string); + free(g_decode_ctx); + g_decode_ctx = NULL; + } + + view_set_draw_callback(app->view_about, NULL); + view_set_input_callback(app->view_about, NULL); + widget_reset(app->widget); +} \ No newline at end of file