mirror of
https://protopirate.net/ProtoPirate/ProtoPirate.git
synced 2026-11-12 11:18:18 +00:00
1487 lines
59 KiB
C
1487 lines
59 KiB
C
#ifdef PROTOPIRATE_SUB_DECODE_PLUGIN_BUILD
|
|
#include "../../protopirate_app_i.h"
|
|
#include "../../helpers/raw_file_reader.h"
|
|
#include "../../helpers/protopirate_storage.h"
|
|
#include "../../helpers/protopirate_views.h"
|
|
#include "../../protocols/protopirate_protocol_plugins.h"
|
|
#include "pp_sd_icons.h"
|
|
#include <dialogs/dialogs.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <lib/subghz/types.h>
|
|
|
|
#define TAG "PPSubDecode"
|
|
|
|
static const ProtoPirateSharedPluginHostApi* g_shared_plugin_host_api = NULL;
|
|
|
|
#define SUBGHZ_APP_FOLDER EXT_PATH("subghz")
|
|
|
|
#define SAMPLES_TO_READ_PER_TICK 512
|
|
|
|
#define SUB_DECODE_MAX_FILE_SIZE (2U * 1024U * 1024U)
|
|
#define SUB_DECODE_PRESET_NAME_MAX 48U
|
|
#define SUB_DECODE_CUSTOM_PRESET_MAX 1024U
|
|
|
|
#define STATE_EMULATE 0
|
|
#define STATE_BF 1
|
|
|
|
// Decode state machine
|
|
typedef enum {
|
|
DecodeStateIdle,
|
|
DecodeStateOpenFile,
|
|
DecodeStateReadHeader,
|
|
DecodeStateStartingWorker,
|
|
DecodeStateDecodingRaw,
|
|
DecodeStateShowHistory,
|
|
DecodeStateShowSignalInfo,
|
|
DecodeStateShowFailure,
|
|
DecodeStateDone,
|
|
} DecodeState;
|
|
|
|
typedef enum {
|
|
SubDecodeFailureGeneric,
|
|
SubDecodeFailureCancelled,
|
|
SubDecodeFailureNotRaw,
|
|
SubDecodeFailureNoMatch,
|
|
} SubDecodeFailureKind;
|
|
|
|
// Context for the whole decode operation
|
|
typedef struct {
|
|
uint64_t decode_elapsed_us;
|
|
char* file_path;
|
|
char* protocol_name;
|
|
char* result;
|
|
char* error_info;
|
|
ProtoPirateHistory* history;
|
|
Storage* storage;
|
|
FlipperFormat* ff;
|
|
uint8_t* previous_preset_data;
|
|
uint8_t* custom_preset_data;
|
|
RawFileReader* raw_reader;
|
|
uint32_t frequency;
|
|
uint32_t previous_frequency;
|
|
size_t previous_preset_data_size;
|
|
size_t custom_preset_data_size;
|
|
DecodeState state;
|
|
SubDecodeFailureKind failure_kind;
|
|
uint16_t signal_count;
|
|
uint16_t selected_history_index;
|
|
uint8_t worker_startup_delay;
|
|
uint8_t owns_history : 1;
|
|
uint8_t showing_signal_info : 1;
|
|
uint8_t previous_preset_saved : 1;
|
|
uint8_t free_result : 1;
|
|
uint8_t free_file_path : 1;
|
|
uint8_t reserved : 3;
|
|
char previous_preset_name[SUB_DECODE_PRESET_NAME_MAX];
|
|
} SubDecodeContext;
|
|
|
|
static SubDecodeContext* g_decode_ctx = NULL;
|
|
|
|
static inline void clear_file_path(bool new_on_heap) {
|
|
if(g_decode_ctx->file_path && g_decode_ctx->free_file_path) free(g_decode_ctx->file_path);
|
|
g_decode_ctx->file_path = NULL;
|
|
g_decode_ctx->free_file_path = new_on_heap;
|
|
}
|
|
|
|
static inline void set_file_path_to_string(char* buffer, bool on_heap) {
|
|
clear_file_path(on_heap);
|
|
g_decode_ctx->file_path = buffer;
|
|
}
|
|
|
|
static inline void clear_result(bool new_on_heap) {
|
|
if(g_decode_ctx->result && g_decode_ctx->free_result) free(g_decode_ctx->result);
|
|
g_decode_ctx->result = NULL;
|
|
g_decode_ctx->free_result = new_on_heap;
|
|
}
|
|
|
|
static inline void set_result_to_string(char* buffer, bool on_heap) {
|
|
clear_result(on_heap);
|
|
g_decode_ctx->result = buffer;
|
|
}
|
|
|
|
static inline void set_error_info_to_string(char* buffer) {
|
|
g_decode_ctx->error_info = buffer;
|
|
}
|
|
|
|
static void protopirate_sub_decode_clear_custom_preset(SubDecodeContext* ctx) {
|
|
if(ctx && ctx->custom_preset_data) {
|
|
free(ctx->custom_preset_data);
|
|
ctx->custom_preset_data = NULL;
|
|
ctx->custom_preset_data_size = 0U;
|
|
}
|
|
}
|
|
|
|
static bool protopirate_sub_decode_try_load_custom_preset(
|
|
SubDecodeContext* ctx,
|
|
FlipperFormat* flipper_format) {
|
|
if(ctx->custom_preset_data && ctx->custom_preset_data_size > 0U) {
|
|
return true;
|
|
}
|
|
|
|
protopirate_sub_decode_clear_custom_preset(ctx);
|
|
|
|
uint32_t value_count = 0;
|
|
flipper_format_rewind(flipper_format);
|
|
if(!flipper_format_get_value_count(flipper_format, "Custom_preset_data", &value_count) ||
|
|
value_count == 0U || value_count > SUB_DECODE_CUSTOM_PRESET_MAX) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t* preset_data = malloc(value_count);
|
|
if(!preset_data) {
|
|
return false;
|
|
}
|
|
|
|
flipper_format_rewind(flipper_format);
|
|
if(!flipper_format_read_hex(flipper_format, "Custom_preset_data", preset_data, value_count)) {
|
|
free(preset_data);
|
|
return false;
|
|
}
|
|
|
|
ctx->custom_preset_data = preset_data;
|
|
ctx->custom_preset_data_size = value_count;
|
|
return true;
|
|
}
|
|
|
|
static const char* protopirate_sub_decode_find_matching_preset_name(
|
|
ProtoPirateApp* app,
|
|
const uint8_t* preset_data,
|
|
size_t preset_data_size) {
|
|
if(!app || !app->setting || !preset_data || preset_data_size == 0U) {
|
|
return NULL;
|
|
}
|
|
|
|
const size_t preset_count = subghz_setting_get_preset_count(app->setting);
|
|
for(size_t i = 0; i < preset_count; i++) {
|
|
uint8_t* candidate_data = subghz_setting_get_preset_data(app->setting, i);
|
|
size_t candidate_size = subghz_setting_get_preset_data_size(app->setting, i);
|
|
if(candidate_data && candidate_size == preset_data_size &&
|
|
memcmp(candidate_data, preset_data, preset_data_size) == 0) {
|
|
return subghz_setting_get_preset_name(app->setting, i);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void protopirate_scene_sub_decode_update_receiver_statusbar(
|
|
ProtoPirateApp* app,
|
|
const SubDecodeContext* ctx) {
|
|
char frequency_str[16] = {0};
|
|
char modulation_str[8] = {0};
|
|
char history_stat_str[16] = {0};
|
|
|
|
g_shared_plugin_host_api->get_frequency_modulation_str(
|
|
app, frequency_str, 16, modulation_str, 8);
|
|
if(ctx && ctx->frequency > 0U) {
|
|
snprintf(
|
|
frequency_str,
|
|
20,
|
|
"%03lu.%02lu",
|
|
(unsigned long)((ctx->frequency / 1000000UL) % 1000UL),
|
|
(unsigned long)((ctx->frequency / 10000UL) % 100UL));
|
|
}
|
|
|
|
const uint16_t signal_count =
|
|
(ctx && ctx->history) ? protopirate_history_get_item(ctx->history) : 0U;
|
|
snprintf(history_stat_str, 16, "%u/%u", signal_count, PROTOPIRATE_HISTORY_MAX);
|
|
|
|
bool is_external =
|
|
app->txrx->radio_device ?
|
|
g_shared_plugin_host_api->radio_device_is_external(app->txrx->radio_device) :
|
|
false;
|
|
|
|
g_shared_plugin_host_api->receiver_add_data_statusbar(
|
|
app->protopirate_receiver,
|
|
frequency_str,
|
|
16,
|
|
modulation_str,
|
|
8,
|
|
history_stat_str,
|
|
16,
|
|
is_external);
|
|
}
|
|
|
|
static void protopirate_scene_sub_decode_update_receiver_progress(
|
|
ProtoPirateApp* app,
|
|
const SubDecodeContext* ctx) {
|
|
uint8_t progress = 0;
|
|
bool should_update = true;
|
|
if(ctx) {
|
|
if(ctx->state == DecodeStateShowHistory || ctx->state == DecodeStateShowSignalInfo ||
|
|
ctx->state == DecodeStateDone) {
|
|
progress = 100;
|
|
} else if(ctx->raw_reader) {
|
|
progress = raw_file_reader_get_progress(ctx->raw_reader);
|
|
} else if(ctx->state == DecodeStateShowFailure) {
|
|
should_update = false;
|
|
}
|
|
}
|
|
if(should_update) {
|
|
g_shared_plugin_host_api->receiver_set_sub_decode_progress(
|
|
app->protopirate_receiver, progress);
|
|
}
|
|
}
|
|
|
|
void protopirate_subdecode_bruteforce_complete_refresh(ProtoPirateApp* app) {
|
|
ProtoPirateApp* a = (ProtoPirateApp*)app;
|
|
SubDecodeContext* ctx = g_decode_ctx;
|
|
if(!a || !ctx) return;
|
|
ctx->state = DecodeStateShowSignalInfo;
|
|
view_dispatcher_send_custom_event(a->view_dispatcher, ProtoPirateCustomEventSubDecodeUpdate);
|
|
}
|
|
|
|
// Forward declaration
|
|
static void protopirate_scene_sub_decode_widget_callback(
|
|
GuiButtonType result,
|
|
InputType type,
|
|
void* context);
|
|
|
|
// Receiver view callback for history navigation
|
|
static void
|
|
protopirate_scene_sub_decode_receiver_callback(ProtoPirateCustomEvent event, void* context);
|
|
|
|
// Callback when receiver successfully decodes a signal
|
|
static void protopirate_sub_decode_receiver_callback(
|
|
SubGhzReceiver* receiver,
|
|
SubGhzProtocolDecoderBase* decoder_base,
|
|
void* context) {
|
|
UNUSED(receiver);
|
|
ProtoPirateApp* app = context;
|
|
SubDecodeContext* ctx = g_decode_ctx;
|
|
|
|
if(!ctx || ctx->state != DecodeStateDecodingRaw) {
|
|
return;
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "=== SIGNAL DECODED FROM FILE ===");
|
|
|
|
const uint32_t virtual_tick = (uint32_t)(ctx->decode_elapsed_us / 1000ULL);
|
|
if(protopirate_history_add_to_history_at(
|
|
ctx->history, decoder_base, app->txrx->preset, virtual_tick)) {
|
|
ctx->signal_count++;
|
|
FURI_LOG_I(TAG, "Added signal %u to history", ctx->signal_count);
|
|
notification_message(app->notifications, &sequence_semi_success);
|
|
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventSubDecodeUpdate);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
static bool protopirate_scene_sub_decode_is_active_decode(const SubDecodeContext* ctx) {
|
|
if(!ctx) return false;
|
|
|
|
return ctx->state == DecodeStateOpenFile || ctx->state == DecodeStateReadHeader ||
|
|
ctx->state == DecodeStateStartingWorker || ctx->state == DecodeStateDecodingRaw;
|
|
}
|
|
|
|
static bool
|
|
protopirate_scene_sub_decode_cancel_active_decode(ProtoPirateApp* app, SubDecodeContext* ctx) {
|
|
if(!app || !ctx || !protopirate_scene_sub_decode_is_active_decode(ctx)) {
|
|
return false;
|
|
}
|
|
|
|
if(ctx->raw_reader) {
|
|
raw_file_reader_free(ctx->raw_reader);
|
|
ctx->raw_reader = NULL;
|
|
}
|
|
close_file_handles(ctx);
|
|
|
|
if(app->txrx && app->txrx->receiver) {
|
|
subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, NULL);
|
|
}
|
|
|
|
set_error_info_to_string("Cancelled");
|
|
set_result_to_string("Decode cancelled", false);
|
|
ctx->failure_kind = SubDecodeFailureCancelled;
|
|
ctx->state = DecodeStateShowFailure;
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
protopirate_scene_sub_decode_receiver_callback(ProtoPirateCustomEvent event, void* context) {
|
|
ProtoPirateApp* app = context;
|
|
view_dispatcher_send_custom_event(app->view_dispatcher, event);
|
|
}
|
|
|
|
static void protopirate_scene_sub_decode_widget_callback(
|
|
GuiButtonType result,
|
|
InputType type,
|
|
void* context) {
|
|
ProtoPirateApp* app = context;
|
|
|
|
if(type == InputTypeShort || type == InputTypeLong) {
|
|
if(result == GuiButtonTypeCenter) {
|
|
SubDecodeContext* ctx = g_decode_ctx;
|
|
if(ctx && !ctx->showing_signal_info) {
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventViewReceiverBack);
|
|
}
|
|
} else if(result == GuiButtonTypeRight) {
|
|
// Save button in signal info view
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventSubDecodeSave);
|
|
} else if(result == GuiButtonTypeLeft) {
|
|
#ifdef ENABLE_EMULATE_FEATURE
|
|
const uint32_t left_event =
|
|
(scene_manager_get_scene_state(app->scene_manager, ProtoPirateSceneSubDecode) ==
|
|
STATE_BF) ?
|
|
ProtoPirateCustomEventBruteforceStart :
|
|
ProtoPirateCustomEventSubDecodeEmulate;
|
|
#else
|
|
const uint32_t left_event = ProtoPirateCustomEventBruteforceStart;
|
|
#endif
|
|
view_dispatcher_send_custom_event(app->view_dispatcher, left_event);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
protopirate_scene_sub_decode_draw_failure(ProtoPirateApp* app, const SubDecodeContext* ctx) {
|
|
widget_reset(app->widget);
|
|
|
|
const char* title = "Decode failed";
|
|
char body[88] = {0};
|
|
|
|
switch(ctx ? ctx->failure_kind : SubDecodeFailureGeneric) {
|
|
case SubDecodeFailureCancelled:
|
|
title = "Decode cancelled";
|
|
snprintf(body, sizeof(body), "Decode stopped!\nNothing saved.");
|
|
break;
|
|
case SubDecodeFailureNotRaw:
|
|
title = "RAW file needed";
|
|
snprintf(body, sizeof(body), "Needs a SubGhz\nRAW capture\nfile.");
|
|
break;
|
|
case SubDecodeFailureNoMatch:
|
|
title = "No match";
|
|
if(ctx && ctx->frequency > 0U) {
|
|
snprintf(
|
|
body,
|
|
sizeof(body),
|
|
"%03lu.%02lu MHz\nNo ProtoPirate\nprotocol detected.",
|
|
(unsigned long)((ctx->frequency / 1000000UL) % 1000UL),
|
|
(unsigned long)((ctx->frequency / 10000UL) % 100UL));
|
|
} else {
|
|
snprintf(body, sizeof(body), "No ProtoPirate\nprotocol detected\nin this signal.");
|
|
}
|
|
break;
|
|
default:
|
|
snprintf(
|
|
body,
|
|
sizeof(body),
|
|
"%s",
|
|
(ctx && strlen(ctx->result)) ? ctx->result : "Try another RAW file.");
|
|
break;
|
|
}
|
|
|
|
widget_add_string_element(app->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, title);
|
|
widget_add_icon_element(app->widget, 2, 16, &I_WarningDolphin_45x42);
|
|
widget_add_string_multiline_element(
|
|
app->widget, 50, 15, AlignLeft, AlignTop, FontSecondary, body);
|
|
widget_add_button_element(
|
|
app->widget, GuiButtonTypeCenter, "OK", protopirate_scene_sub_decode_widget_callback, app);
|
|
}
|
|
|
|
static bool protopirate_scene_sub_decode_select_file(SubDecodeContext* ctx) {
|
|
DialogsFileBrowserOptions browser_options;
|
|
dialog_file_browser_set_basic_options(&browser_options, ".sub", &I_subghz_10px);
|
|
browser_options.base_path = SUBGHZ_APP_FOLDER;
|
|
browser_options.hide_ext = false;
|
|
|
|
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
|
if(ctx->file_path && strlen(ctx->file_path)) {
|
|
set_file_path_to_string(SUBGHZ_APP_FOLDER, false);
|
|
}
|
|
|
|
FuriString* file_path = furi_string_alloc_set_str(ctx->file_path);
|
|
const bool selected =
|
|
dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);
|
|
const char* buffer = strdup(furi_string_get_cstr(file_path));
|
|
set_file_path_to_string((char*)buffer, true);
|
|
furi_string_free(file_path);
|
|
furi_record_close(RECORD_DIALOGS);
|
|
|
|
if(selected) {
|
|
FURI_LOG_I(TAG, "Selected file: %s", ctx->file_path);
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
static void protopirate_scene_sub_decode_prepare_receiver_view(ProtoPirateApp* app) {
|
|
g_shared_plugin_host_api->receiver_set_sub_decode_mode(app->protopirate_receiver, true);
|
|
g_shared_plugin_host_api->receiver_reset_menu(app->protopirate_receiver);
|
|
g_shared_plugin_host_api->receiver_set_idx_menu(app->protopirate_receiver, 0);
|
|
g_shared_plugin_host_api->receiver_set_callback(
|
|
app->protopirate_receiver, protopirate_scene_sub_decode_receiver_callback, app);
|
|
protopirate_scene_sub_decode_update_receiver_progress(app, g_decode_ctx);
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, g_decode_ctx);
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewReceiver);
|
|
}
|
|
|
|
static void protopirate_scene_sub_decode_reset_for_file(SubDecodeContext* ctx) {
|
|
if(ctx->raw_reader) {
|
|
raw_file_reader_free(ctx->raw_reader);
|
|
ctx->raw_reader = NULL;
|
|
}
|
|
close_file_handles(ctx);
|
|
protopirate_sub_decode_clear_custom_preset(ctx);
|
|
protopirate_history_reset(ctx->history);
|
|
|
|
if(ctx->protocol_name) {
|
|
free(ctx->protocol_name);
|
|
ctx->protocol_name = NULL;
|
|
}
|
|
clear_result(false);
|
|
ctx->error_info = NULL;
|
|
ctx->failure_kind = SubDecodeFailureGeneric;
|
|
ctx->signal_count = 0;
|
|
ctx->selected_history_index = 0;
|
|
ctx->showing_signal_info = false;
|
|
ctx->worker_startup_delay = 0;
|
|
ctx->decode_elapsed_us = 0;
|
|
}
|
|
|
|
static bool protopirate_scene_sub_decode_open_browser_for_next_file(ProtoPirateApp* app) {
|
|
SubDecodeContext* ctx = g_decode_ctx;
|
|
if(!ctx) return false;
|
|
|
|
const bool selected = protopirate_scene_sub_decode_select_file(ctx);
|
|
if(!selected) {
|
|
return false;
|
|
}
|
|
|
|
protopirate_scene_sub_decode_reset_for_file(ctx);
|
|
ctx->state = DecodeStateOpenFile;
|
|
protopirate_scene_sub_decode_prepare_receiver_view(app);
|
|
return true;
|
|
}
|
|
|
|
static void protopirate_scene_sub_decode_text_input_callback(void* context) {
|
|
ProtoPirateApp* app = context;
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventSubDecodeSaveConfirm);
|
|
}
|
|
|
|
void plugin_scene_sub_decode_on_enter(ProtoPirateApp* app) {
|
|
if(!g_shared_plugin_host_api->ensure_receiver_view(app) ||
|
|
!g_shared_plugin_host_api->ensure_widget(app)) {
|
|
notification_message(app->notifications, &sequence_error);
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
return;
|
|
}
|
|
|
|
if(!app->radio_initialized && !g_shared_plugin_host_api->radio_init(app)) {
|
|
FURI_LOG_E(TAG, "Failed to initialize radio for sub decode scene");
|
|
notification_message(app->notifications, &sequence_error);
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
return;
|
|
}
|
|
|
|
if(app->txrx && app->txrx->history) {
|
|
protopirate_history_release_scratch(app->txrx->history);
|
|
}
|
|
|
|
g_shared_plugin_host_api->rx_stack_resume_after_tx(app);
|
|
if(!app->txrx->receiver) {
|
|
FURI_LOG_E(TAG, "Failed to allocate receiver for sub decode scene");
|
|
notification_message(app->notifications, &sequence_error);
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
return;
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "Sub decode scene enter - Free heap: %zu", memmgr_get_free_heap());
|
|
|
|
g_decode_ctx = malloc(sizeof(SubDecodeContext));
|
|
if(!g_decode_ctx) {
|
|
FURI_LOG_E(TAG, "Failed to allocate decode context");
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
return;
|
|
}
|
|
memset(g_decode_ctx, 0, sizeof(SubDecodeContext));
|
|
|
|
if(app->txrx && app->txrx->preset && app->txrx->preset->name) {
|
|
snprintf(
|
|
g_decode_ctx->previous_preset_name,
|
|
sizeof(g_decode_ctx->previous_preset_name),
|
|
"%s",
|
|
furi_string_get_cstr(app->txrx->preset->name));
|
|
g_decode_ctx->previous_frequency = app->txrx->preset->frequency;
|
|
g_decode_ctx->previous_preset_data = app->txrx->preset->data;
|
|
g_decode_ctx->previous_preset_data_size = app->txrx->preset->data_size;
|
|
g_decode_ctx->previous_preset_saved = true;
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "After decode context alloc - Free heap: %zu", memmgr_get_free_heap());
|
|
|
|
// Allocate history
|
|
bool owns_history = false;
|
|
if(!app->txrx->history) {
|
|
app->txrx->history = protopirate_history_alloc();
|
|
if(!app->txrx->history) {
|
|
FURI_LOG_E(TAG, "Failed to allocate history!");
|
|
free(g_decode_ctx);
|
|
g_decode_ctx = NULL;
|
|
notification_message(app->notifications, &sequence_error);
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
return;
|
|
}
|
|
owns_history = true;
|
|
}
|
|
|
|
g_decode_ctx->file_path = NULL;
|
|
g_decode_ctx->protocol_name = NULL;
|
|
clear_result(false);
|
|
g_decode_ctx->error_info = NULL;
|
|
g_decode_ctx->state = DecodeStateIdle;
|
|
g_decode_ctx->history = app->txrx->history;
|
|
g_decode_ctx->owns_history = owns_history;
|
|
//protopirate_history_reset(g_decode_ctx->history);
|
|
g_decode_ctx->signal_count = 0;
|
|
g_decode_ctx->selected_history_index = 0;
|
|
g_decode_ctx->raw_reader = NULL;
|
|
g_decode_ctx->worker_startup_delay = 0;
|
|
|
|
FURI_LOG_I(TAG, "After context setup - Free heap: %zu", memmgr_get_free_heap());
|
|
|
|
set_file_path_to_string(SUBGHZ_APP_FOLDER, false);
|
|
|
|
if(protopirate_scene_sub_decode_select_file(g_decode_ctx)) {
|
|
protopirate_scene_sub_decode_reset_for_file(g_decode_ctx);
|
|
g_decode_ctx->state = DecodeStateOpenFile;
|
|
protopirate_scene_sub_decode_prepare_receiver_view(app);
|
|
} else {
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
}
|
|
}
|
|
|
|
bool plugin_scene_sub_decode_on_event(ProtoPirateApp* app, SceneManagerEvent event) {
|
|
SubDecodeContext* ctx = g_decode_ctx;
|
|
|
|
if(!ctx) return false;
|
|
bool consumed = false;
|
|
if(event.type == SceneManagerEventTypeCustom) {
|
|
if(event.event == ProtoPirateCustomEventSubDecodeUpdate) {
|
|
// Update receiver view with new history items (when signals are detected during decoding)
|
|
if(ctx->state == DecodeStateDecodingRaw) {
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, ctx);
|
|
consumed = true;
|
|
} else if(
|
|
ctx->state == DecodeStateShowHistory ||
|
|
(ctx->state == DecodeStateDone && !ctx->showing_signal_info)) {
|
|
// Rebuild history view
|
|
uint16_t history_count = protopirate_history_get_item(ctx->history);
|
|
if(history_count > 0) {
|
|
g_shared_plugin_host_api->receiver_sync_menu_from_history(
|
|
app->protopirate_receiver, ctx->history);
|
|
|
|
g_shared_plugin_host_api->receiver_set_idx_menu(
|
|
app->protopirate_receiver, ctx->selected_history_index);
|
|
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, ctx);
|
|
}
|
|
}
|
|
consumed = true;
|
|
} else if(event.event == ProtoPirateCustomEventSubDecodeSave) {
|
|
// Save the file (same as receiver_info)
|
|
FlipperFormat* ff =
|
|
protopirate_history_get_raw_data(ctx->history, ctx->selected_history_index);
|
|
if(ff) {
|
|
#define YMD_LENGTH 17
|
|
char* file_name_str = malloc(PROTOPIRATE_PROTOCOL_NAME_MAX + YMD_LENGTH + 1);
|
|
if(app->datetime_filenames) {
|
|
//Get the date and time to save.
|
|
DateTime date_time;
|
|
furi_hal_rtc_get_datetime(&date_time);
|
|
snprintf(
|
|
file_name_str,
|
|
PROTOPIRATE_PROTOCOL_NAME_MAX + YMD_LENGTH,
|
|
"%.2d%.2d%.2d_%.2d.%.2d.%.2d_",
|
|
date_time.year,
|
|
date_time.month,
|
|
date_time.day,
|
|
date_time.hour,
|
|
date_time.minute,
|
|
date_time.second);
|
|
}
|
|
|
|
// Extract protocol name
|
|
FuriString* buffer = furi_string_alloc();
|
|
protopirate_storage_get_capture_display_protocol(ff, buffer);
|
|
|
|
//Add the protocol
|
|
char* file_name_dup = strdup(file_name_str);
|
|
snprintf(
|
|
file_name_str,
|
|
PROTOPIRATE_PROTOCOL_NAME_MAX + YMD_LENGTH,
|
|
"%s%s",
|
|
file_name_dup,
|
|
furi_string_get_cstr(buffer));
|
|
free(file_name_dup);
|
|
|
|
// Clean protocol name for filename
|
|
for(char* p = file_name_str; *p; p++) {
|
|
if(*p == '/' || *p == ' ') *p = '_';
|
|
}
|
|
|
|
// Get the next auto-generated filename (just the name part)
|
|
g_shared_plugin_host_api->free_text_input(app);
|
|
furi_string_reset(buffer);
|
|
if(protopirate_storage_get_next_filename(
|
|
file_name_str, buffer, app->datetime_filenames)) {
|
|
// Extract just the filename without folder and extension
|
|
const char* full = furi_string_get_cstr(buffer);
|
|
const char* slash = strrchr(full, '/');
|
|
const char* name_start = slash ? slash + 1 : full;
|
|
|
|
// Copy without extension
|
|
size_t name_len = strlen(name_start);
|
|
const char* dot = strrchr(name_start, '.');
|
|
if(dot) name_len = dot - name_start;
|
|
if(name_len > 64) name_len = 64;
|
|
app->save_filename = malloc(name_len + 1);
|
|
memcpy(app->save_filename, name_start, name_len);
|
|
} else {
|
|
uint8_t len = 8;
|
|
app->save_filename = malloc(len);
|
|
snprintf(app->save_filename, len, "capture");
|
|
}
|
|
furi_string_free(buffer);
|
|
|
|
// Store context for when text input confirms
|
|
app->save_history_idx = app->txrx->idx_menu_chosen;
|
|
|
|
g_shared_plugin_host_api->ensure_text_input(app);
|
|
|
|
// Configure and show text input
|
|
text_input_set_header_text(app->text_input, "Save filename:");
|
|
text_input_set_result_callback(
|
|
app->text_input,
|
|
protopirate_scene_sub_decode_text_input_callback,
|
|
app,
|
|
app->save_filename,
|
|
strlen(app->save_filename),
|
|
false); // don't clear default text
|
|
|
|
app->dialog_showing = true;
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewTextInput);
|
|
free(file_name_str);
|
|
} else {
|
|
FURI_LOG_E(
|
|
TAG,
|
|
"No flipper format data available, item: %d",
|
|
ctx->selected_history_index);
|
|
notification_message(app->notifications, &sequence_error);
|
|
}
|
|
consumed = true;
|
|
} else if(event.event == ProtoPirateCustomEventSubDecodeSaveConfirm) {
|
|
// User confirmed the filename in text input
|
|
FlipperFormat* ff =
|
|
protopirate_history_get_raw_data(app->txrx->history, app->save_history_idx);
|
|
if(ff) {
|
|
// Build full path: folder/filename.psf
|
|
FuriString* save_path = furi_string_alloc_printf(
|
|
"%s/%s%s",
|
|
PROTOPIRATE_APP_FOLDER,
|
|
app->save_filename,
|
|
PROTOPIRATE_APP_EXTENSION);
|
|
|
|
if(protopirate_storage_save_capture_to_path(ff, furi_string_get_cstr(save_path))) {
|
|
notification_message(app->notifications, &sequence_success);
|
|
FURI_LOG_I(TAG, "Saved to: %s", furi_string_get_cstr(save_path));
|
|
} else {
|
|
notification_message(app->notifications, &sequence_error);
|
|
FURI_LOG_E(TAG, "Save failed");
|
|
}
|
|
furi_string_free(save_path);
|
|
}
|
|
|
|
app->dialog_showing = false;
|
|
|
|
// Return to the receiver info widget
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
|
|
|
|
//Kill the text_input view.
|
|
g_shared_plugin_host_api->free_text_input(app);
|
|
consumed = true;
|
|
|
|
}
|
|
#ifdef ENABLE_EMULATE_FEATURE
|
|
else if(
|
|
event.event == ProtoPirateCustomEventSubDecodeEmulate &&
|
|
app->emulate_feature_enabled && !app->emulate_disabled_for_loaded) {
|
|
FlipperFormat* ff =
|
|
protopirate_history_get_raw_data(ctx->history, ctx->selected_history_index);
|
|
if(ff && protopirate_storage_save_capture_to_path(ff, PROTOPIRATE_TEMP_FILE)) {
|
|
protopirate_history_release_scratch(ctx->history);
|
|
if(app->loaded_file_path) free(app->loaded_file_path);
|
|
size_t len = strlen(PROTOPIRATE_TEMP_FILE) + 1;
|
|
app->loaded_file_path = malloc(len);
|
|
snprintf(app->loaded_file_path, len, PROTOPIRATE_TEMP_FILE);
|
|
|
|
FURI_LOG_I(TAG, "Emulate from sub-decode temp file: %s", app->loaded_file_path);
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateEmulate);
|
|
} else {
|
|
FURI_LOG_E(
|
|
TAG, "Failed to prepare emulate capture %u", ctx->selected_history_index);
|
|
notification_message(app->notifications, &sequence_error);
|
|
}
|
|
consumed = true;
|
|
}
|
|
#endif
|
|
else if(event.event == ProtoPirateCustomEventBruteforceStart) {
|
|
app->txrx->idx_menu_chosen = ctx->selected_history_index;
|
|
if(g_shared_plugin_host_api->bruteforce_plugin_ensure_loaded(app) &&
|
|
app->running_bruteforce_plugin.bruteforce_plugin &&
|
|
app->running_bruteforce_plugin.bruteforce_plugin->on_scene_event(
|
|
app, ProtoPirateBruteForceContextSubDecode, event)) {
|
|
if(app->running_bruteforce_plugin.bruteforce_plugin->is_running(app)) {
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
|
|
}
|
|
}
|
|
consumed = true;
|
|
return consumed;
|
|
} else if(event.event == ProtoPirateCustomEventBruteforceComplete) {
|
|
app->txrx->idx_menu_chosen = ctx->selected_history_index;
|
|
if(app->running_bruteforce_plugin.bruteforce_plugin) {
|
|
app->running_bruteforce_plugin.bruteforce_plugin->on_scene_event(
|
|
app, ProtoPirateBruteForceContextSubDecode, event);
|
|
}
|
|
ctx->state = DecodeStateShowSignalInfo;
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventSubDecodeUpdate);
|
|
consumed = true;
|
|
return consumed;
|
|
} else if(event.event == ProtoPirateCustomEventViewReceiverOK) {
|
|
// User selected a signal from history - show signal info
|
|
uint16_t idx =
|
|
g_shared_plugin_host_api->receiver_get_idx_menu(app->protopirate_receiver);
|
|
uint16_t history_count = protopirate_history_get_item(ctx->history);
|
|
if(idx < history_count) {
|
|
ctx->selected_history_index = idx;
|
|
ctx->state = DecodeStateShowSignalInfo;
|
|
ctx->showing_signal_info = true;
|
|
// Trigger state handler
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventSubDecodeUpdate);
|
|
}
|
|
consumed = true;
|
|
} else if(event.event == ProtoPirateCustomEventViewReceiverBack) {
|
|
if(protopirate_scene_sub_decode_cancel_active_decode(app, ctx)) {
|
|
consumed = true;
|
|
return consumed;
|
|
}
|
|
|
|
if(!protopirate_scene_sub_decode_open_browser_for_next_file(app)) {
|
|
protopirate_history_reset(ctx->history);
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventPluginNavigateBack);
|
|
}
|
|
consumed = true;
|
|
}
|
|
}
|
|
|
|
if(event.type == SceneManagerEventTypeTick) {
|
|
consumed = true;
|
|
|
|
app->txrx->idx_menu_chosen = ctx->selected_history_index;
|
|
if(app->running_bruteforce_plugin.bruteforce_plugin &&
|
|
app->running_bruteforce_plugin.bruteforce_plugin->is_running(app) &&
|
|
app->running_bruteforce_plugin.bruteforce_plugin->on_scene_event(
|
|
app, ProtoPirateBruteForceContextSubDecode, event)) {
|
|
return consumed;
|
|
}
|
|
|
|
switch(ctx->state) {
|
|
case DecodeStateOpenFile: {
|
|
FURI_LOG_I(TAG, "OpenFile: Starting - Free heap: %zu", memmgr_get_free_heap());
|
|
ctx->storage = furi_record_open(RECORD_STORAGE);
|
|
FURI_LOG_D(TAG, "OpenFile: Storage opened");
|
|
ctx->ff = flipper_format_file_alloc(ctx->storage);
|
|
FURI_LOG_D(TAG, "OpenFile: FlipperFormat allocated");
|
|
|
|
if(!flipper_format_file_open_existing(ctx->ff, ctx->file_path)) {
|
|
FURI_LOG_E(TAG, "OpenFile: Failed to open file");
|
|
set_result_to_string("Failed to open file", false);
|
|
set_error_info_to_string("File open failed");
|
|
close_file_handles(ctx);
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
} else {
|
|
FURI_LOG_I(TAG, "OpenFile: File opened successfully");
|
|
ctx->state = DecodeStateReadHeader;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case DecodeStateReadHeader: {
|
|
FURI_LOG_I(TAG, "ReadHeader: Starting - Free heap: %zu", memmgr_get_free_heap());
|
|
|
|
FuriString* temp_str = furi_string_alloc();
|
|
uint32_t version = 0;
|
|
bool success = false;
|
|
|
|
do {
|
|
FURI_LOG_D(TAG, "ReadHeader: Reading header");
|
|
if(!flipper_format_read_header(ctx->ff, temp_str, &version)) {
|
|
set_result_to_string("Invalid file format", false);
|
|
set_error_info_to_string("Invalid header");
|
|
break;
|
|
}
|
|
|
|
FURI_LOG_D(TAG, "ReadHeader: Header type: %s", furi_string_get_cstr(temp_str));
|
|
if(furi_string_cmp_str(temp_str, "Flipper SubGhz RAW File") != 0) {
|
|
set_result_to_string("Not a RAW SubGhz file", false);
|
|
set_error_info_to_string("Not RAW SubGhz file");
|
|
ctx->failure_kind = SubDecodeFailureNotRaw;
|
|
break;
|
|
}
|
|
|
|
FURI_LOG_D(TAG, "ReadHeader: Reading protocol");
|
|
if(!flipper_format_read_string(ctx->ff, FF_PROTOCOL, temp_str)) {
|
|
set_result_to_string("Missing Protocol", false);
|
|
set_error_info_to_string("No protocol field");
|
|
break;
|
|
} else {
|
|
if(ctx->protocol_name) {
|
|
free(ctx->protocol_name);
|
|
//ctx->protocol_name = NULL;
|
|
}
|
|
size_t len = furi_string_size(temp_str);
|
|
ctx->protocol_name = malloc(len);
|
|
snprintf(ctx->protocol_name, len, "%s", furi_string_get_cstr(temp_str));
|
|
}
|
|
|
|
FURI_LOG_D(TAG, "ReadHeader: Rewinding for frequency");
|
|
flipper_format_rewind(ctx->ff);
|
|
flipper_format_read_header(ctx->ff, temp_str, &version);
|
|
ctx->frequency = 433920000;
|
|
flipper_format_read_uint32(ctx->ff, FF_FREQUENCY, &ctx->frequency, 1);
|
|
|
|
FURI_LOG_I(TAG, "Protocol: %s, Freq: %lu", ctx->protocol_name, ctx->frequency);
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, ctx);
|
|
|
|
success = true;
|
|
} while(false);
|
|
|
|
furi_string_free(temp_str);
|
|
FURI_LOG_D(TAG, "ReadHeader: Freed temp_str");
|
|
|
|
if(!success) {
|
|
FURI_LOG_E(TAG, "ReadHeader: Failed, closing handles");
|
|
close_file_handles(ctx);
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
} else if(ctx->protocol_name && strcmp(ctx->protocol_name, "RAW")) {
|
|
FURI_LOG_I(TAG, "ReadHeader: RAW file detected, closing handles");
|
|
close_file_handles(ctx);
|
|
FURI_LOG_D(TAG, "ReadHeader: Handles closed");
|
|
|
|
ctx->state = DecodeStateStartingWorker;
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"ReadHeader: State set to StartingWorker - Free heap: %zu",
|
|
memmgr_get_free_heap());
|
|
} else {
|
|
FURI_LOG_W(TAG, "ReadHeader: Non-RAW protocol not supported");
|
|
close_file_handles(ctx);
|
|
set_error_info_to_string("Only RAW supported");
|
|
ctx->failure_kind = SubDecodeFailureNotRaw;
|
|
ctx->state = DecodeStateShowFailure;
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "ReadHeader: Complete, next state: %d", ctx->state);
|
|
break;
|
|
}
|
|
|
|
case DecodeStateStartingWorker: {
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"StartingWorker: Entry - delay=%u, Free heap: %zu",
|
|
ctx->worker_startup_delay,
|
|
memmgr_get_free_heap());
|
|
|
|
if(ctx->worker_startup_delay < 3) {
|
|
ctx->worker_startup_delay++;
|
|
FURI_LOG_D(TAG, "StartingWorker: Delay tick %u", ctx->worker_startup_delay);
|
|
break;
|
|
}
|
|
ctx->worker_startup_delay = 0;
|
|
|
|
FURI_LOG_I(TAG, "StartingWorker: Reading file metadata");
|
|
|
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
if(!storage) {
|
|
FURI_LOG_E(TAG, "Failed to open storage");
|
|
break;
|
|
}
|
|
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
|
|
if(!fff_data_file) {
|
|
FURI_LOG_E(TAG, "Failed to allocate FlipperFormat");
|
|
furi_record_close(RECORD_STORAGE);
|
|
break;
|
|
}
|
|
|
|
FuriString* temp_str = furi_string_alloc();
|
|
bool setup_ok = false;
|
|
|
|
do {
|
|
if(!flipper_format_file_open_existing(fff_data_file, ctx->file_path)) {
|
|
FURI_LOG_E(TAG, "Error opening file for metadata");
|
|
break;
|
|
}
|
|
|
|
/*uint32_t version = 0;
|
|
if(!flipper_format_read_header(fff_data_file, temp_str, &version)) {
|
|
FURI_LOG_E(TAG, "Missing or incorrect header");
|
|
break;
|
|
}
|
|
|
|
if(strcmp(furi_string_get_cstr(temp_str), "Flipper SubGhz RAW File") != 0 ||
|
|
version != 1) {
|
|
FURI_LOG_E(TAG, "Not a valid RAW file");
|
|
break;
|
|
}*/
|
|
|
|
if(!flipper_format_read_uint32(fff_data_file, FF_FREQUENCY, &ctx->frequency, 1)) {
|
|
FURI_LOG_E(TAG, "Missing Frequency");
|
|
break;
|
|
}
|
|
|
|
if(!flipper_format_read_string(fff_data_file, FF_PRESET, temp_str)) {
|
|
FURI_LOG_E(TAG, "Missing Preset");
|
|
break;
|
|
}
|
|
|
|
const char* preset_name_long = furi_string_get_cstr(temp_str);
|
|
const char* preset_name_short = pp_get_short_preset_name(preset_name_long);
|
|
uint8_t* preset_data = NULL;
|
|
size_t preset_data_size = 0U;
|
|
|
|
if(pp_preset_name_is_custom_marker(preset_name_long)) {
|
|
if(protopirate_sub_decode_try_load_custom_preset(ctx, fff_data_file)) {
|
|
preset_data = ctx->custom_preset_data;
|
|
preset_data_size = ctx->custom_preset_data_size;
|
|
const char* matched_preset_name =
|
|
protopirate_sub_decode_find_matching_preset_name(
|
|
app, preset_data, preset_data_size);
|
|
if(matched_preset_name) {
|
|
preset_name_short = matched_preset_name;
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"Loaded RAW custom preset data (%u bytes), matched preset %s",
|
|
(unsigned)preset_data_size,
|
|
preset_name_short);
|
|
} else {
|
|
preset_name_short = "Custom";
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"Loaded RAW custom preset data (%u bytes), no preset-name match",
|
|
(unsigned)preset_data_size);
|
|
}
|
|
} else {
|
|
FURI_LOG_W(TAG, "RAW custom preset data missing, falling back to AM650");
|
|
preset_name_short = "AM650";
|
|
}
|
|
}
|
|
|
|
if(!preset_data) {
|
|
size_t preset_index = subghz_setting_get_preset_count(app->setting);
|
|
for(size_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
|
|
if(!strcmp(
|
|
subghz_setting_get_preset_name(app->setting, i),
|
|
preset_name_short)) {
|
|
preset_index = i;
|
|
break;
|
|
}
|
|
}
|
|
if(preset_index >= subghz_setting_get_preset_count(app->setting)) {
|
|
preset_name_short = "AM650";
|
|
for(size_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
|
|
if(!strcmp(
|
|
subghz_setting_get_preset_name(app->setting, i),
|
|
preset_name_short)) {
|
|
preset_index = i;
|
|
break;
|
|
}
|
|
}
|
|
if(preset_index >= subghz_setting_get_preset_count(app->setting)) {
|
|
FURI_LOG_E(TAG, "Failed to get preset index!");
|
|
break;
|
|
}
|
|
}
|
|
|
|
preset_data = subghz_setting_get_preset_data(app->setting, preset_index);
|
|
preset_data_size =
|
|
subghz_setting_get_preset_data_size(app->setting, preset_index);
|
|
}
|
|
|
|
if(!preset_data || preset_data_size == 0U) {
|
|
FURI_LOG_E(TAG, "Failed to get preset data!");
|
|
break;
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "Sub-decode using preset %s", preset_name_short);
|
|
|
|
g_shared_plugin_host_api->preset_init(
|
|
app, preset_name_short, ctx->frequency, preset_data, preset_data_size);
|
|
|
|
if(!g_shared_plugin_host_api->apply_protocol_registry_for_context(
|
|
app, preset_name_short, ctx->frequency, preset_data, preset_data_size, NULL) ||
|
|
!app->txrx->receiver) {
|
|
FURI_LOG_E(TAG, "Failed to rebuild receiver for preset %s", preset_name_short);
|
|
break;
|
|
}
|
|
|
|
subghz_receiver_reset(app->txrx->receiver);
|
|
subghz_receiver_set_rx_callback(
|
|
app->txrx->receiver, protopirate_sub_decode_receiver_callback, app);
|
|
|
|
setup_ok = true;
|
|
} while(false);
|
|
|
|
if(fff_data_file) flipper_format_free(fff_data_file);
|
|
|
|
if(storage) furi_record_close(RECORD_STORAGE);
|
|
|
|
furi_string_free(temp_str);
|
|
|
|
if(!setup_ok) {
|
|
set_result_to_string("Failed to read file metadata", false);
|
|
set_error_info_to_string("Metadata read failed");
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
break;
|
|
}
|
|
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"StartingWorker: Allocating raw reader - Free heap: %zu",
|
|
memmgr_get_free_heap());
|
|
|
|
ctx->raw_reader = raw_file_reader_alloc();
|
|
if(!ctx->raw_reader) {
|
|
FURI_LOG_E(TAG, "Failed to allocate raw reader");
|
|
set_result_to_string("Memory allocation failed", false);
|
|
set_error_info_to_string("Out of memory");
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
break;
|
|
}
|
|
|
|
{
|
|
Storage* size_storage = furi_record_open(RECORD_STORAGE);
|
|
FileInfo file_info = {0};
|
|
bool stat_ok =
|
|
size_storage &&
|
|
(storage_common_stat(size_storage, ctx->file_path, &file_info) == FSE_OK);
|
|
uint64_t file_size = stat_ok ? file_info.size : 0;
|
|
if(size_storage) {
|
|
furi_record_close(RECORD_STORAGE);
|
|
}
|
|
|
|
if(stat_ok && file_size > SUB_DECODE_MAX_FILE_SIZE) {
|
|
FURI_LOG_W(TAG, "RAW file too large: %llu bytes", file_size);
|
|
raw_file_reader_free(ctx->raw_reader);
|
|
ctx->raw_reader = NULL;
|
|
|
|
char* buffer = malloc(56);
|
|
snprintf(
|
|
buffer,
|
|
56,
|
|
"File too large to decode\n\n"
|
|
"%lu KB\n"
|
|
"(limit %lu KB)",
|
|
(uint32_t)(file_size / 1024U),
|
|
(uint32_t)(SUB_DECODE_MAX_FILE_SIZE / 1024U));
|
|
set_result_to_string(buffer, true);
|
|
set_error_info_to_string("File too large");
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
break;
|
|
}
|
|
}
|
|
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"StartingWorker: Opening RAW stream - Free heap: %zu",
|
|
memmgr_get_free_heap());
|
|
|
|
if(!raw_file_reader_open(ctx->raw_reader, ctx->file_path)) {
|
|
FURI_LOG_E(TAG, "Failed to open raw file");
|
|
raw_file_reader_free(ctx->raw_reader);
|
|
set_result_to_string("Failed to open RAW file", false);
|
|
set_error_info_to_string("File open failed");
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
break;
|
|
}
|
|
|
|
ctx->state = DecodeStateDecodingRaw;
|
|
ctx->decode_elapsed_us = 0;
|
|
protopirate_scene_sub_decode_update_receiver_progress(app, ctx);
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, ctx);
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"StartingWorker: Ready to decode RAW stream - Free heap: %zu",
|
|
memmgr_get_free_heap());
|
|
break;
|
|
}
|
|
|
|
case DecodeStateDecodingRaw: {
|
|
if(!ctx->raw_reader) {
|
|
FURI_LOG_E(TAG, "DecodingRaw: No raw reader");
|
|
ctx->state = DecodeStateShowFailure;
|
|
break;
|
|
}
|
|
|
|
uint32_t duration = 0;
|
|
uint32_t samples_processed = 0;
|
|
bool level = false;
|
|
|
|
while(samples_processed < SAMPLES_TO_READ_PER_TICK) {
|
|
if(!raw_file_reader_get_next(ctx->raw_reader, &level, &duration)) {
|
|
const bool at_eof = raw_file_reader_is_finished(ctx->raw_reader);
|
|
FURI_LOG_I(
|
|
TAG,
|
|
"DecodingRaw: Read stopped, eof=%d, signals=%u",
|
|
at_eof,
|
|
ctx->signal_count);
|
|
|
|
raw_file_reader_free(ctx->raw_reader);
|
|
ctx->raw_reader = NULL;
|
|
|
|
subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, NULL);
|
|
|
|
if(!at_eof) {
|
|
set_result_to_string("Failed while reading RAW file", false);
|
|
set_error_info_to_string("Read interrupted");
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
break;
|
|
}
|
|
|
|
uint16_t history_count = protopirate_history_get_item(ctx->history);
|
|
g_shared_plugin_host_api->receiver_set_sub_decode_progress(
|
|
app->protopirate_receiver, 100);
|
|
|
|
if(history_count > 0) {
|
|
ctx->state = DecodeStateShowHistory;
|
|
ctx->selected_history_index = 0;
|
|
ctx->showing_signal_info = false;
|
|
notification_message(app->notifications, &sequence_success);
|
|
} else {
|
|
char* buffer = malloc(75);
|
|
snprintf(
|
|
buffer,
|
|
75,
|
|
"RAW Signal\n\n"
|
|
"Freq: %lu.%02lu MHz\n\n"
|
|
"No ProtoPirate protocol\n"
|
|
"detected in signal.",
|
|
ctx->frequency / 1000000,
|
|
(ctx->frequency % 1000000) / 10000);
|
|
|
|
set_result_to_string(buffer, true);
|
|
set_error_info_to_string("No protocol match");
|
|
ctx->failure_kind = SubDecodeFailureNoMatch;
|
|
ctx->state = DecodeStateShowFailure;
|
|
notification_message(app->notifications, &sequence_error);
|
|
}
|
|
break;
|
|
}
|
|
ctx->decode_elapsed_us += duration;
|
|
subghz_receiver_decode(app->txrx->receiver, level, duration);
|
|
samples_processed++;
|
|
}
|
|
|
|
protopirate_scene_sub_decode_update_receiver_progress(app, ctx);
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, ctx);
|
|
break;
|
|
}
|
|
|
|
case DecodeStateShowFailure: {
|
|
protopirate_scene_sub_decode_draw_failure(app, ctx);
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
|
|
ctx->state = DecodeStateDone;
|
|
break;
|
|
}
|
|
|
|
case DecodeStateShowHistory: {
|
|
// Show history list using receiver view (same as receive mode)
|
|
uint16_t history_count = protopirate_history_get_item(ctx->history);
|
|
if(history_count > 0) {
|
|
g_shared_plugin_host_api->receiver_sync_menu_from_history(
|
|
app->protopirate_receiver, ctx->history);
|
|
|
|
// Set initial selection
|
|
g_shared_plugin_host_api->receiver_set_idx_menu(
|
|
app->protopirate_receiver, ctx->selected_history_index);
|
|
|
|
// Set up callback
|
|
g_shared_plugin_host_api->receiver_set_callback(
|
|
app->protopirate_receiver,
|
|
protopirate_scene_sub_decode_receiver_callback,
|
|
app);
|
|
|
|
g_shared_plugin_host_api->receiver_set_sub_decode_progress(
|
|
app->protopirate_receiver, 100);
|
|
protopirate_scene_sub_decode_update_receiver_statusbar(app, ctx);
|
|
|
|
// Switch to receiver view
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewReceiver);
|
|
ctx->state = DecodeStateDone;
|
|
ctx->showing_signal_info = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case DecodeStateShowSignalInfo: {
|
|
// Show signal info in widget (same layout as receiver_info)
|
|
widget_reset(app->widget);
|
|
|
|
uint16_t history_count = protopirate_history_get_item(ctx->history);
|
|
if(ctx->selected_history_index < history_count) {
|
|
FuriString* text = furi_string_alloc();
|
|
|
|
// Get menu text (first line) for header
|
|
/*protopirate_history_get_text_item_menu(
|
|
ctx->history, text, ctx->selected_history_index);
|
|
widget_add_string_element(
|
|
app->widget,
|
|
64,
|
|
0,
|
|
AlignCenter,
|
|
AlignTop,
|
|
FontPrimary,
|
|
furi_string_get_cstr(text));*/
|
|
|
|
// Get full text for body
|
|
furi_string_reset(text);
|
|
protopirate_history_get_text_item_detail(
|
|
ctx->history, ctx->selected_history_index, text, app->txrx->environment);
|
|
widget_add_text_scroll_element(
|
|
app->widget, 0, 0, 128, 50, furi_string_get_cstr(text));
|
|
|
|
// Add save button
|
|
widget_add_button_element(
|
|
app->widget,
|
|
GuiButtonTypeRight,
|
|
"Save",
|
|
protopirate_scene_sub_decode_widget_callback,
|
|
app);
|
|
|
|
bool left_button_bf = false;
|
|
app->emulate_disabled_for_loaded = true;
|
|
|
|
// Store reference to history item's flipper format for saving
|
|
FlipperFormat* ff =
|
|
protopirate_history_get_raw_data(ctx->history, ctx->selected_history_index);
|
|
if(ff) {
|
|
FuriString* proto_str = furi_string_alloc();
|
|
flipper_format_rewind(ff);
|
|
bool have_proto = flipper_format_read_string(ff, FF_PROTOCOL, proto_str);
|
|
bool offers_bf = have_proto && protopirate_protocol_catalog_offers_bruteforce(
|
|
furi_string_get_cstr(proto_str));
|
|
|
|
if(have_proto) {
|
|
const char* protocol_name = furi_string_get_cstr(proto_str);
|
|
app->emulate_disabled_for_loaded =
|
|
!protopirate_protocol_catalog_can_tx(protocol_name);
|
|
} else {
|
|
app->emulate_disabled_for_loaded = true;
|
|
}
|
|
furi_string_free(proto_str);
|
|
if(offers_bf) {
|
|
app->txrx->idx_menu_chosen = ctx->selected_history_index;
|
|
bool needs_bf = false;
|
|
if(g_shared_plugin_host_api->bruteforce_plugin_ensure_loaded(app) &&
|
|
app->running_bruteforce_plugin.bruteforce_plugin) {
|
|
needs_bf =
|
|
app->running_bruteforce_plugin.bruteforce_plugin->needs_bruteforce(
|
|
ff);
|
|
} else {
|
|
//Show the user the error.
|
|
widget_add_button_element(
|
|
app->widget, GuiButtonTypeLeft, "(Error)", NULL, app);
|
|
left_button_bf = true;
|
|
needs_bf = false;
|
|
}
|
|
g_shared_plugin_host_api->bruteforce_plugin_unload_if_idle(app);
|
|
if(needs_bf) {
|
|
scene_manager_set_scene_state(
|
|
app->scene_manager, ProtoPirateSceneSubDecode, STATE_BF);
|
|
|
|
widget_add_button_element(
|
|
app->widget,
|
|
GuiButtonTypeLeft,
|
|
"BF",
|
|
protopirate_scene_sub_decode_widget_callback,
|
|
app);
|
|
left_button_bf = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef ENABLE_EMULATE_FEATURE
|
|
if(!left_button_bf && app->emulate_feature_enabled &&
|
|
!app->emulate_disabled_for_loaded) {
|
|
widget_add_button_element(
|
|
app->widget,
|
|
GuiButtonTypeLeft,
|
|
"Emulate",
|
|
protopirate_scene_sub_decode_widget_callback,
|
|
app);
|
|
}
|
|
#endif
|
|
|
|
scene_manager_set_scene_state(
|
|
app->scene_manager,
|
|
ProtoPirateSceneSubDecode,
|
|
left_button_bf ? STATE_BF : STATE_EMULATE);
|
|
furi_string_free(text);
|
|
}
|
|
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
|
|
ctx->state = DecodeStateDone;
|
|
ctx->showing_signal_info = true;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
} else if(event.type == SceneManagerEventTypeBack) {
|
|
if(app->dialog_showing) {
|
|
app->dialog_showing = false;
|
|
view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewWidget);
|
|
return false;
|
|
}
|
|
app->txrx->idx_menu_chosen = ctx->selected_history_index;
|
|
if(protopirate_scene_sub_decode_cancel_active_decode(app, ctx)) {
|
|
consumed = true;
|
|
return consumed;
|
|
}
|
|
if(app->running_bruteforce_plugin.bruteforce_plugin &&
|
|
app->running_bruteforce_plugin.bruteforce_plugin->on_scene_event(
|
|
app, ProtoPirateBruteForceContextSubDecode, event)) {
|
|
consumed = true;
|
|
return consumed;
|
|
}
|
|
// Handle back button navigation
|
|
if(ctx->showing_signal_info) {
|
|
// In signal info - go back to history
|
|
ctx->showing_signal_info = false;
|
|
if(app->save_filename) {
|
|
free(app->save_filename);
|
|
app->save_filename = NULL;
|
|
};
|
|
ctx->state = DecodeStateShowHistory;
|
|
view_dispatcher_send_custom_event(
|
|
app->view_dispatcher, ProtoPirateCustomEventSubDecodeUpdate);
|
|
consumed = true;
|
|
}
|
|
// If in history view, back is handled by ViewReceiverBack event
|
|
}
|
|
return consumed;
|
|
}
|
|
|
|
void plugin_scene_sub_decode_on_exit(ProtoPirateApp* app) {
|
|
if(app && app->txrx && app->txrx->receiver) {
|
|
subghz_receiver_reset(app->txrx->receiver);
|
|
subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, NULL);
|
|
}
|
|
|
|
//If the user clicked cancel, these get left allocated.
|
|
g_shared_plugin_host_api->free_text_input(app);
|
|
|
|
g_shared_plugin_host_api->bruteforce_context_release(app);
|
|
|
|
bool owns_history = false;
|
|
if(g_decode_ctx) {
|
|
owns_history = g_decode_ctx->owns_history;
|
|
|
|
if(g_decode_ctx->raw_reader) {
|
|
raw_file_reader_free(g_decode_ctx->raw_reader);
|
|
g_decode_ctx->raw_reader = NULL;
|
|
}
|
|
|
|
close_file_handles(g_decode_ctx);
|
|
|
|
if(g_decode_ctx->previous_preset_saved && app && app->txrx && app->txrx->preset) {
|
|
g_shared_plugin_host_api->preset_init(
|
|
app,
|
|
g_decode_ctx->previous_preset_name,
|
|
g_decode_ctx->previous_frequency,
|
|
g_decode_ctx->previous_preset_data,
|
|
g_decode_ctx->previous_preset_data_size);
|
|
}
|
|
|
|
protopirate_sub_decode_clear_custom_preset(g_decode_ctx);
|
|
clear_file_path(false);
|
|
if(g_decode_ctx->protocol_name) {
|
|
free(g_decode_ctx->protocol_name);
|
|
//g_decode_ctx->protocol_name = NULL;
|
|
}
|
|
clear_result(false);
|
|
//g_decode_ctx->error_info = NULL;
|
|
free(g_decode_ctx);
|
|
g_decode_ctx = NULL;
|
|
}
|
|
|
|
if(owns_history && app && app->txrx && app->txrx->history) {
|
|
protopirate_history_reset(app->txrx->history);
|
|
|
|
FURI_LOG_D(TAG, "Freeing history %p", app->txrx->history);
|
|
protopirate_history_free(app->txrx->history);
|
|
app->txrx->history = NULL;
|
|
}
|
|
|
|
if(app && app->protopirate_receiver) {
|
|
g_shared_plugin_host_api->receiver_reset_menu(app->protopirate_receiver);
|
|
}
|
|
}
|
|
|
|
static void sub_decode_plugin_set_host_api(const ProtoPirateSharedPluginHostApi* host_api) {
|
|
g_shared_plugin_host_api = host_api;
|
|
}
|
|
|
|
static const ProtoPirateSharedPlugin protopirate_sub_decode_plugin = {
|
|
.plugin_name = "",
|
|
.set_host_api = sub_decode_plugin_set_host_api,
|
|
.on_enter = plugin_scene_sub_decode_on_enter,
|
|
.on_event = plugin_scene_sub_decode_on_event,
|
|
.on_exit = plugin_scene_sub_decode_on_exit,
|
|
.release = NULL,
|
|
};
|
|
|
|
static const FlipperAppPluginDescriptor protopirate_sub_decode_plugin_descriptor = {
|
|
.appid = PROTOPIRATE_SUB_DECODE_PLUGIN_APP_ID,
|
|
.ep_api_version = PROTOPIRATE_SUB_DECODE_PLUGIN_API_VERSION,
|
|
.entry_point = &protopirate_sub_decode_plugin,
|
|
};
|
|
|
|
const FlipperAppPluginDescriptor* protopirate_sub_decode_plugin_ep(void) {
|
|
return &protopirate_sub_decode_plugin_descriptor;
|
|
}
|
|
#endif
|