diff --git a/app.h b/app.h index 7397bea8fbe..40b579ff695 100644 --- a/app.h +++ b/app.h @@ -56,9 +56,11 @@ typedef enum { } SwitchViewDirection; typedef struct { - const char *name; - FuriHalSubGhzPreset preset; - uint8_t *custom; + const char *name; // Name to show to the user. + const char *id; // Identifier in the Flipper API/file. + FuriHalSubGhzPreset preset; // The preset ID. + uint8_t *custom; // If not null, a set of registers for + // the CC1101, specifying a custom preset. } ProtoViewModulation; extern ProtoViewModulation ProtoViewModulations[]; /* In app_subghz.c */ @@ -200,6 +202,9 @@ uint32_t convert_from_diff_manchester(uint8_t *buf, uint64_t buflen, uint8_t *bi void init_msg_info(ProtoViewMsgInfo *i, ProtoViewApp *app); void free_msg_info(ProtoViewMsgInfo *i); +/* signal_file.c */ +bool save_signal(ProtoViewApp *app, const char *filename); + /* view_*.c */ void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app); void process_input_raw_pulses(ProtoViewApp *app, InputEvent input); diff --git a/app_subghz.c b/app_subghz.c index ec7724b137b..840b683dce4 100644 --- a/app_subghz.c +++ b/app_subghz.c @@ -13,17 +13,19 @@ void raw_sampling_worker_start(ProtoViewApp *app); void raw_sampling_worker_stop(ProtoViewApp *app); ProtoViewModulation ProtoViewModulations[] = { - {"OOK 650Khz", FuriHalSubGhzPresetOok650Async, NULL}, - {"OOK 270Khz", FuriHalSubGhzPresetOok270Async, NULL}, - {"2FSK 2.38Khz", FuriHalSubGhzPreset2FSKDev238Async, NULL}, - {"2FSK 47.6Khz", FuriHalSubGhzPreset2FSKDev476Async, NULL}, - {"MSK", FuriHalSubGhzPresetMSK99_97KbAsync, NULL}, - {"GFSK", FuriHalSubGhzPresetGFSK9_99KbAsync, NULL}, - {"TPMS 1 (FSK)", 0, (uint8_t*)protoview_subghz_tpms1_fsk_async_regs}, - {"TPMS 2 (OOK)", 0, (uint8_t*)protoview_subghz_tpms2_ook_async_regs}, - {"TPMS 3 (FSK)", 0, (uint8_t*)protoview_subghz_tpms3_fsk_async_regs}, - {"TPMS 4 (FSK)", 0, (uint8_t*)protoview_subghz_tpms4_fsk_async_regs}, - {NULL, 0, NULL} /* End of list sentinel. */ + {"OOK 650Khz", "FuriHalSubGhzPresetOok650Async", + FuriHalSubGhzPresetOok650Async, NULL}, + {"OOK 270Khz", "FuriHalSubGhzPresetOok270Async", + FuriHalSubGhzPresetOok270Async, NULL}, + {"2FSK 2.38Khz", "FuriHalSubGhzPreset2FSKDev238Async", + FuriHalSubGhzPreset2FSKDev238Async, NULL}, + {"2FSK 47.6Khz", "FuriHalSubGhzPreset2FSKDev476Async", + FuriHalSubGhzPreset2FSKDev476Async, NULL}, + {"TPMS 1 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms1_fsk_async_regs}, + {"TPMS 2 (OOK)", NULL, 0, (uint8_t*)protoview_subghz_tpms2_ook_async_regs}, + {"TPMS 3 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms3_fsk_async_regs}, + {"TPMS 4 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms4_fsk_async_regs}, + {NULL, NULL, 0, NULL} /* End of list sentinel. */ }; /* Called after the application initialization in order to setup the diff --git a/signal.c b/signal.c index 0eba3cd1c1d..bb823f7f468 100644 --- a/signal.c +++ b/signal.c @@ -147,6 +147,7 @@ void scan_for_signal(ProtoViewApp *app) { * fill, in case it is able to decode a message. */ ProtoViewMsgInfo *info = malloc(sizeof(ProtoViewMsgInfo)); init_msg_info(info,app); + info->short_pulse_dur = copy->short_pulse_dur; uint32_t saved_idx = copy->idx; /* Save index, see later. */ @@ -523,7 +524,6 @@ void free_msg_info(ProtoViewMsgInfo *i) { void init_msg_info(ProtoViewMsgInfo *i, ProtoViewApp *app) { UNUSED(app); memset(i,0,sizeof(ProtoViewMsgInfo)); - i->short_pulse_dur = DetectedSamples->short_pulse_dur; i->bits = NULL; } diff --git a/signal_file.c b/signal_file.c new file mode 100644 index 00000000000..0800a2cf885 --- /dev/null +++ b/signal_file.c @@ -0,0 +1,143 @@ +/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved + * Copyright (C) 2023 Maciej Wojtasik -- All Rights Reserved + * See the LICENSE file for information about the license. */ + +#include "app.h" +#include +#include + +/* ========================= Signal file operations ========================= */ + +/* This function saves the current logical signal on disk. What is saved here + * is not the signal as level and duration as we received it from CC1101, + * but it's logical representation stored in the app->msg_info bitmap, where + * each 1 or 0 means a puls or gap for the specified short pulse duration time + * (te). */ +bool save_signal(ProtoViewApp *app, const char *filename) { + /* We have a message at all? */ + if (app->msg_info == NULL || app->msg_info->pulses_count == 0) return false; + + Storage *storage = furi_record_open(RECORD_STORAGE); + FlipperFormat *file = flipper_format_file_alloc(storage); + Stream *stream = flipper_format_get_raw_stream(file); + FuriString *file_content = NULL; + bool success = true; + + if (flipper_format_file_open_always(file, filename)) { + /* Write the file header. */ + FuriString *file_content = furi_string_alloc(); + const char *preset_id = ProtoViewModulations[app->modulation].id; + + furi_string_printf(file_content, + "Filetype: Flipper SubGhz RAW File\n" + "Version: 1\n" + "Frequency: %ld\n" + "Preset: %s\n", + app->frequency, + preset_id ? preset_id : "FuriHalSubGhzPresetCustom"); + + /* For custom modulations, we need to emit a set of registers. */ + if (preset_id == NULL) { + FuriString *custom = furi_string_alloc(); + uint8_t *regs = ProtoViewModulations[app->modulation].custom; + furi_string_printf(custom, + "Custom_preset_module: CC1101\n" + "Custom_preset_data: "); + for (int j = 0; regs[j]; j += 2) { + furi_string_cat_printf(custom, "%02X %02X ", + (int)regs[j], (int)regs[j+1]); + } + size_t len = furi_string_size(file_content); + furi_string_set_char(custom,len-1,'\n'); + furi_string_cat(file_content,custom); + furi_string_free(custom); + } + + /* We always save raw files. */ + furi_string_cat_printf(file_content, + "Protocol: RAW\n" + "RAW_Data: -10000\n"); // Start with 10 ms of gap + + /* Write header. */ + size_t len = furi_string_size(file_content); + if (stream_write(stream, + (uint8_t*) furi_string_get_cstr(file_content), len) + != len) + { + FURI_LOG_W(TAG, "Short write to file"); + success = false; + goto write_err; + } + furi_string_reset(file_content); + + /* Write raw data sections. The Flipper subghz parser can't handle + * too much data on a single line, so we generate a new one + * every few samples. */ + uint32_t this_line_samples = 0; + uint32_t max_line_samples = 100; + uint32_t idx = 0; // Iindex in the signal bitmap. + ProtoViewMsgInfo *i = app->msg_info; + FURI_LOG_W(TAG, "short dur:%d", (int)i->short_pulse_dur); + while(idx < i->pulses_count) { + bool level = bitmap_get(i->bits,i->bits_bytes,idx); + uint32_t te_times = 1; + idx++; + /* Count the duration of the current pulse/gap. */ + while(idx < i->pulses_count && + bitmap_get(i->bits,i->bits_bytes,idx) == level) + { + te_times++; + idx++; + } + // Invariant: after the loop 'idx' is at the start of the + // next gap or pulse. + + int32_t dur = (int32_t)i->short_pulse_dur * te_times; + if (level == 0) dur = -dur; /* Negative is gap in raw files. */ + + /* Emit the sample. If this is the first sample of the line, + * also emit the RAW_Data: field. */ + if (this_line_samples == 0) + furi_string_cat_printf(file_content,"RAW_Data: "); + furi_string_cat_printf(file_content,"%d ",(int)dur); + FURI_LOG_W(TAG, "dur:%d/%d at idx %d", (int)dur, (int)i->pulses_count, (int)idx); + this_line_samples++; + + /* Store the current set of samples on disk, when we reach a + * given number or the end of the signal. */ + bool end_reached = (idx == i->pulses_count); + if (this_line_samples == max_line_samples || end_reached) { + /* If that's the end, terminate the signal with a long + * gap. */ + if (end_reached) furi_string_cat_printf(file_content,"-10000 "); + + /* We always have a trailing space in the last sample. Make it + * a newline. */ + size_t len = furi_string_size(file_content); + furi_string_set_char(file_content,len-1,'\n'); + + if (stream_write(stream, + (uint8_t*) furi_string_get_cstr(file_content), + len) != len) + { + FURI_LOG_W(TAG, "Short write to file"); + success = false; + goto write_err; + } + + /* Prepare for next line. */ + furi_string_reset(file_content); + this_line_samples = 0; + } + } + } else { + success = false; + FURI_LOG_W(TAG, "Unable to open file"); + } + +write_err: + furi_record_close(RECORD_STORAGE); + flipper_format_free(file); + if (file_content != NULL) furi_string_free(file_content); + return success; +} diff --git a/view_info.c b/view_info.c index 40e06c36d3a..5ce7de5f0a6 100644 --- a/view_info.c +++ b/view_info.c @@ -94,9 +94,17 @@ void render_view_info(Canvas *const canvas, ProtoViewApp *app) { } } +/* The user typed the file name. Let's save it and remove the keyboard + * view. */ void text_input_done_callback(void* context) { ProtoViewApp *app = context; InfoViewPrivData *privdata = app->view_privdata; + + FuriString *save_path = furi_string_alloc_printf( + "%s/%s.sub", EXT_PATH("subghz"), privdata->filename); + save_signal(app, furi_string_get_cstr(save_path)); + furi_string_free(save_path); + free(privdata->filename); dismiss_keyboard(app); } @@ -118,6 +126,7 @@ void set_signal_random_filename(ProtoViewApp *app, char *buf, size_t buflen) { snprintf(buf,buflen,"%.10s-%s-%d",app->msg_info->name,suffix,rand()%1000); str_replace(buf,' ','_'); str_replace(buf,'-','_'); + str_replace(buf,'/','_'); } /* Handle input for the info view. */