فهرست منبع

Merge branch 'save_subview'

antirez 3 سال پیش
والد
کامیت
a04f4fb90f
18فایلهای تغییر یافته به همراه670 افزوده شده و 82 حذف شده
  1. 0 20
      TODO
  2. 60 4
      app.c
  3. 52 9
      app.h
  4. 13 11
      app_subghz.c
  5. 8 2
      protocols/b4b1.c
  6. 7 3
      protocols/keeloq.c
  7. 2 0
      protocols/oregon2.c
  8. 3 0
      protocols/tpms/citroen.c
  9. 3 0
      protocols/tpms/ford.c
  10. 4 1
      protocols/tpms/renault.c
  11. 4 1
      protocols/tpms/schrader.c
  12. 3 0
      protocols/tpms/schrader_eg53ma4.c
  13. 16 2
      protocols/tpms/toyota.c
  14. 140 12
      signal.c
  15. 141 0
      signal_file.c
  16. 74 0
      ui.c
  17. 139 16
      view_info.c
  18. 1 1
      view_raw_signal.c

+ 0 - 20
TODO

@@ -1,20 +0,0 @@
-Core improvements
-=================
-
-- Decoders should declare the short pulse duration range, so that
-  only matching decoders will be called. This may also be useful for
-  modulations. If a signal is only OOK, does not make much sense to
-  call it for samples obtained in FSK.
-- More protocols, especially TPMS and other stuff not supported right now
-  by the Flipper.
-- CC1101 synchronous mode with protocol hopping?
-- Protocols decoded can register actions, for instance to generate
-  sub files with modified signal and so forth.
-- Optimize memory usage storing raw samples in a bitfield: 15 bits
-  duration, 1 bit level.
-
-Roadmap
-=======
-
-- Signal saving, editing, resending.
-- Subviews system.

+ 60 - 4
app.c

@@ -95,6 +95,12 @@ static void app_switch_view(ProtoViewApp *app, SwitchViewDirection dir) {
     if ((old == ViewFrequencySettings && new != ViewModulationSettings) ||
         (old == ViewModulationSettings && new != ViewFrequencySettings))
         view_exit_settings(app);
+
+    /* Set the current subview of the view we just left to zero, that is
+     * the main subview of the view. When re re-enter it we want to see
+     * the main thing. */
+    app->current_subview[old] = 0;
+    memset(app->view_privdata,0,PROTOVIEW_VIEW_PRIVDATA_LEN);
 }
 
 /* Allocate the application state and initialize a number of stuff.
@@ -117,8 +123,14 @@ ProtoViewApp* protoview_app_alloc() {
     view_port_input_callback_set(app->view_port, input_callback, app);
     gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
     app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+    app->view_dispatcher = NULL;
+    app->text_input = NULL;
+    app->show_text_input = false;
     app->current_view = ViewRawPulses;
+    for (int j = 0; j < ViewLast; j++) app->current_subview[j] = 0;
     app->direct_sampling_enabled = false;
+    app->view_privdata = malloc(PROTOVIEW_VIEW_PRIVDATA_LEN);
+    memset(app->view_privdata,0,PROTOVIEW_VIEW_PRIVDATA_LEN);
 
     // Signal found and visualization defaults
     app->signal_bestlen = 0;
@@ -126,8 +138,9 @@ ProtoViewApp* protoview_app_alloc() {
     app->signal_decoded = false;
     app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
     app->signal_offset = 0;
+    app->msg_info = NULL;
 
-    //init Worker & Protocol
+    // Init Worker & Protocol
     app->txrx = malloc(sizeof(ProtoViewTxRx));
 
     /* Setup rx worker and environment. */
@@ -252,12 +265,14 @@ int32_t protoview_app_entry(void* p) {
                 /* Exit the app. */
                 app->running = 0;
             } else if (input.type == InputTypeShort &&
-                       input.key == InputKeyRight)
+                       input.key == InputKeyRight &&
+                       get_current_subview(app) == 0)
             {
                 /* Go to the next view. */
                 app_switch_view(app,AppNextView);
             } else if (input.type == InputTypeShort &&
-                       input.key == InputKeyLeft)
+                       input.key == InputKeyLeft &&
+                       get_current_subview(app) == 0)
             {
                 /* Go to the previous view. */
                 app_switch_view(app,AppPrevView);
@@ -289,7 +304,48 @@ int32_t protoview_app_entry(void* p) {
                 if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
             }
         }
-        view_port_update(app->view_port);
+        if (app->show_text_input) {
+            /* Remove our viewport: we need to use a view dispatcher
+             * in order to show the standard Flipper keyboard. */
+            gui_remove_view_port(app->gui, app->view_port);
+
+            /* Allocate a view dispatcher, add a text input view to it,
+             * and activate it. */
+            app->view_dispatcher = view_dispatcher_alloc();
+            view_dispatcher_enable_queue(app->view_dispatcher);
+            app->text_input = text_input_alloc();
+            view_dispatcher_set_event_callback_context(app->view_dispatcher,app);
+            view_dispatcher_add_view(app->view_dispatcher, 0, text_input_get_view(app->text_input));
+            view_dispatcher_switch_to_view(app->view_dispatcher, 0);
+
+            /* Setup the text input view. The different parameters are set
+             * in the app structure by the view that wanted to show the
+             * input text. The callback, buffer and buffer len must be set.  */
+            text_input_set_header_text(app->text_input, "Save signal filename");
+            text_input_set_result_callback(
+                app->text_input,
+                app->text_input_done_callback,
+                app,
+                app->text_input_buffer,
+                app->text_input_buffer_len,
+                false);
+
+            /* Run the dispatcher with the keyboard. */
+            view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+            view_dispatcher_run(app->view_dispatcher);
+
+            /* Undo all it: remove the view from the dispatcher, free it
+             * so that it removes itself from the current gui, finally
+             * restore our viewport. */
+            view_dispatcher_remove_view(app->view_dispatcher, 0);
+            text_input_free(app->text_input);
+            view_dispatcher_free(app->view_dispatcher);
+            app->view_dispatcher = NULL;
+            gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+            app->show_text_input = false;
+        } else {
+            view_port_update(app->view_port);
+        }
     }
 
     /* App no longer running. Shut down and free. */

+ 52 - 9
app.h

@@ -14,6 +14,7 @@
 #include <gui/modules/submenu.h>
 #include <gui/modules/variable_item_list.h>
 #include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
 #include <notification/notification_messages.h>
 #include <lib/subghz/subghz_setting.h>
 #include <lib/subghz/subghz_worker.h>
@@ -23,8 +24,9 @@
 #include "app_buffer.h"
 
 #define TAG "ProtoView"
-#define PROTOVIEW_RAW_VIEW_DEFAULT_SCALE 100
-#define BITMAP_SEEK_NOT_FOUND UINT32_MAX
+#define PROTOVIEW_RAW_VIEW_DEFAULT_SCALE 100 // 100us is 1 pixel by default
+#define BITMAP_SEEK_NOT_FOUND UINT32_MAX // Returned by function as sentinel
+#define PROTOVIEW_VIEW_PRIVDATA_LEN 64 // View specific private data len
 
 #define DEBUG_MSG 1
 
@@ -54,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 */
@@ -97,7 +101,18 @@ typedef struct ProtoViewMsgInfo {
     char info2[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 2. */
     char info3[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 3. */
     char info4[PROTOVIEW_MSG_STR_LEN]; /* Protocol specific info line 4. */
-    uint64_t len;       /* Bits consumed from the stream. */
+    /* Low level information of the detected signal: the following are filled
+     * by the protocol decoding function: */
+    uint32_t start_off;         /* Pulses start offset in the bitmap. */
+    uint32_t pulses_count;      /* Number of pulses of the full message. */
+    /* The following are passed already filled to the decoder. */
+    uint32_t short_pulse_dur;   /* Microseconds duration of the short pulse. */
+    /* The following are filled by ProtoView core after the decoder returned
+     * success. */
+    uint8_t *bits;              /* Bitmap with the signal. */
+    uint32_t bits_bytes;        /* Number of full bytes in the bitmap, that
+                                   is 'pulses_count/8' rounded to the next
+                                   integer. */
 } ProtoViewMsgInfo;
 
 struct ProtoViewApp {
@@ -105,8 +120,17 @@ struct ProtoViewApp {
     Gui *gui;
     ViewPort *view_port;     /* We just use a raw viewport and we render
                                 everything into the low level canvas. */
-    ProtoViewCurrentView current_view;  /* Active view ID. */
+    ProtoViewCurrentView current_view;      /* Active left-right view ID. */
+    int current_subview[ViewLast];  /* Active up-down subview ID. */
     FuriMessageQueue *event_queue;  /* Keypress events go here. */
+    ViewDispatcher *view_dispatcher; /* Used only when we want to show
+                                        the text_input view for a moment.
+                                        Otherwise it is set to null. */
+    TextInput *text_input;
+    bool show_text_input;
+    char *text_input_buffer;
+    uint32_t text_input_buffer_len;
+    void (*text_input_done_callback)(void*);
 
     /* Radio related. */
     ProtoViewTxRx *txrx;     /* Radio state. */
@@ -118,9 +142,16 @@ struct ProtoViewApp {
     uint32_t signal_last_scan_idx; /* Index of the buffer last time we
                                       performed the scan. */
     bool signal_decoded;     /* Was the current signal decoded? */
-    ProtoViewMsgInfo signal_info; /* Decoded message, if signal_decoded true. */
+    ProtoViewMsgInfo *msg_info; /* Decoded message info if not NULL. */
     bool direct_sampling_enabled; /* This special view needs an explicit
                                      acknowledge to work. */
+    void *view_privdata;    /* This is a piece of memory of total size
+                               PROTOVIEW_VIEW_PRIVDATA_LEN that it is
+                               initialized to zero when we switch to
+                               a a new view. While the view we are using
+                               is the same, it can be used by the view to
+                               store any kind of info inside, just casting
+                               the pointer to a few specific-data structure. */
 
     /* Raw view apps state. */
     uint32_t us_scale;       /* microseconds per pixel. */
@@ -161,12 +192,18 @@ void reset_current_signal(ProtoViewApp *app);
 void scan_for_signal(ProtoViewApp *app);
 bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos);
 void bitmap_set(uint8_t *b, uint32_t blen, uint32_t bitpos, bool val);
-void bitmap_set_pattern(uint8_t *b, uint32_t blen, const char *pat);
+void bitmap_copy(uint8_t *d, uint32_t dlen, uint32_t doff, uint8_t *s, uint32_t slen, uint32_t soff, uint32_t count);
+void bitmap_set_pattern(uint8_t *b, uint32_t blen, uint32_t off, const char *pat);
 void bitmap_reverse_bytes(uint8_t *p, uint32_t len);
 bool bitmap_match_bits(uint8_t *b, uint32_t blen, uint32_t bitpos, const char *bits);
 uint32_t bitmap_seek_bits(uint8_t *b, uint32_t blen, uint32_t startpos, uint32_t maxbits, const char *bits);
 uint32_t convert_from_line_code(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t offset, const char *zero_pattern, const char *one_pattern);
 uint32_t convert_from_diff_manchester(uint8_t *buf, uint64_t buflen, uint8_t *bits, uint32_t len, uint32_t off, bool previous);
+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);
@@ -182,7 +219,13 @@ void view_exit_direct_sampling(ProtoViewApp *app);
 void view_exit_settings(ProtoViewApp *app);
 
 /* ui.c */
+int get_current_subview(ProtoViewApp *app);
+void show_available_subviews(Canvas *canvas, ProtoViewApp *app, int last_subview);
+bool process_subview_updown(ProtoViewApp *app, InputEvent input, int last_subview);
 void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color);
+void show_keyboard(ProtoViewApp *app, char *buffer, uint32_t buflen,
+                   void (*done_callback)(void*));
+void dismiss_keyboard(ProtoViewApp *app);
 
 /* crc.c */
 uint8_t crc8(const uint8_t *data, size_t len, uint8_t init, uint8_t poly);

+ 13 - 11
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

+ 8 - 2
protocols/b4b1.c

@@ -25,6 +25,9 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     }
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     if (DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 preamble at: %lu",off);
+    info->start_off = off;
+
+    // Seek data setction. Why -1? Last bit is data.
     off += strlen(sync_patterns[j])-1;
 
     uint8_t d[3]; /* 24 bits of data. */
@@ -32,10 +35,13 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
         convert_from_line_code(d,sizeof(d),bits,numbytes,off,"1000","1110");
 
     if (DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 decoded: %lu",decoded);
-    if (decoded != 24) return false;
+    if (decoded < 24) return false;
+
+    off += 24*4; // seek to end symbol offset to calculate the length.
+    off++; // In this protocol there is a final pulse as terminator.
+    info->pulses_count = off - info->start_off;
     snprintf(info->name,PROTOVIEW_MSG_STR_LEN,"PT/SC remote");
     snprintf(info->raw,PROTOVIEW_MSG_STR_LEN,"%02X%02X%02X",d[0],d[1],d[2]);
-    info->len = off+(4*24);
     return true;
 }
 

+ 7 - 3
protocols/keeloq.c

@@ -32,9 +32,11 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     const char *sync_pattern = "101010101010101010101010" "0000";
     uint8_t sync_len = 24+4;
     if (numbits-sync_len+sync_len < 3*66) return false;
-    uint64_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern);
+    uint32_t off = bitmap_seek_bits(bits,numbytes,0,numbits,sync_pattern);
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
-    off += sync_len;
+
+    info->start_off = off;
+    off += sync_len; // Seek start of message.
 
     /* Now there is half the gap left, but we allow from 3 to 7, instead of 5
      * symbols of gap, to avoid missing the signal for a matter of wrong
@@ -52,8 +54,10 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
         convert_from_line_code(raw,sizeof(raw),bits,numbytes,off,
             "110","100"); /* Pulse width modulation. */
     FURI_LOG_E(TAG, "Keeloq decoded bits: %lu", decoded);
-
     if (decoded < 66) return false; /* Require the full 66 bits. */
+
+    info->pulses_count = (off+66*3) - info->start_off;
+
     bitmap_reverse_bytes(raw,sizeof(raw)); /* Keeloq is LSB first. */
 
     int buttons = raw[7]>>4;

+ 2 - 0
protocols/oregon2.c

@@ -13,6 +13,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     FURI_LOG_E(TAG, "Oregon2 preamble+sync found");
 
+    info->start_off = off;
     off += 32; /* Skip preamble. */
 
     uint8_t buffer[8], raw[8] = {0};
@@ -21,6 +22,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     FURI_LOG_E(TAG, "Oregon2 decoded bits: %lu", decoded);
 
     if (decoded < 11*4) return false; /* Minimum len to extract some data. */
+    info->pulses_count = (off+11*4*4) - info->start_off;
 
     char temp[3] = {0}, deviceid[2] = {0}, hum[2] = {0};
     for (int j = 0; j < 64; j += 4) {

+ 3 - 0
protocols/tpms/citroen.c

@@ -20,6 +20,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     FURI_LOG_E(TAG, "Renault TPMS preamble+sync found");
 
+    info->start_off = off;
     off += sync_len; /* Skip preamble + sync. */
 
     uint8_t raw[10];
@@ -37,6 +38,8 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     for (int j = 1; j < 10; j++) crc ^= raw[j];
     if (crc != 0) return false; /* Require sane checksum. */
 
+    info->pulses_count = (off+8*10*2) - info->start_off;
+
     int repeat = raw[5] & 0xf;
     float kpa = (float)raw[6]*1.364;
     int temp = raw[7]-50;

+ 3 - 0
protocols/tpms/ford.c

@@ -20,6 +20,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     FURI_LOG_E(TAG, "Fort TPMS preamble+sync found");
 
+    info->start_off = off;
     off += sync_len; /* Skip preamble and sync. */
 
     uint8_t raw[8];
@@ -35,6 +36,8 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     for (int j = 0; j < 7; j++) crc += raw[j];
     if (crc != raw[7]) return false; /* Require sane CRC. */
 
+    info->pulses_count = (off+8*8*2) - info->start_off;
+
     float psi = 0.25 * (((raw[6]&0x20)<<3)|raw[4]);
 
     /* Temperature apperas to be valid only if the most significant

+ 4 - 1
protocols/tpms/renault.c

@@ -25,7 +25,7 @@ static const char *test_vector =
 static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) {
 
     if (USE_TEST_VECTOR) { /* Test vector to check that decoding works. */
-        bitmap_set_pattern(bits,numbytes,test_vector);
+        bitmap_set_pattern(bits,numbytes,0,test_vector);
         numbits = strlen(test_vector);
     }
 
@@ -36,6 +36,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     FURI_LOG_E(TAG, "Renault TPMS preamble+sync found");
 
+    info->start_off = off;
     off += 20; /* Skip preamble. */
 
     uint8_t raw[9];
@@ -47,6 +48,8 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (decoded < 8*9) return false; /* Require the full 9 bytes. */
     if (crc8(raw,8,0,7) != raw[8]) return false; /* Require sane CRC. */
 
+    info->pulses_count = (off+8*9*2) - info->start_off;
+
     float kpa = 0.75 *((uint32_t)((raw[0]&3)<<8) | raw[1]);
     int temp = raw[2]-30;
 

+ 4 - 1
protocols/tpms/schrader.c

@@ -16,7 +16,7 @@ static const char *test_vector = "0000001111010101010110100101100101101010010101
 static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo *info) {
 
     if (USE_TEST_VECTOR) { /* Test vector to check that decoding works. */
-        bitmap_set_pattern(bits,numbytes,test_vector);
+        bitmap_set_pattern(bits,numbytes,0,test_vector);
         numbits = strlen(test_vector);
     }
 
@@ -27,6 +27,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     FURI_LOG_E(TAG, "Schrader TPMS gap+preamble found");
 
+    info->start_off = off;
     off += 10; /* Skip just the long pulse and the first 3 bits of sync, so
                   that we have the first byte of data with the sync nibble
                   0011 = 0x3. */
@@ -46,6 +47,8 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
         return false;
     }
 
+    info->pulses_count = (off+8*8*2) - info->start_off;
+
     float kpa = (float)raw[5]*2.5;
     int temp = raw[6]-50;
 

+ 3 - 0
protocols/tpms/schrader_eg53ma4.c

@@ -25,6 +25,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (off == BITMAP_SEEK_NOT_FOUND) return false;
     FURI_LOG_E(TAG, "Schrader EG53MA4 TPMS preamble+sync found");
 
+    info->start_off = off;
     off += sync_len-8; /* Skip preamble, not sync that is part of the data. */
 
     uint8_t raw[10];
@@ -40,6 +41,8 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     for (int j = 0; j < 9; j++) crc += raw[j];
     if (crc != raw[9]) return false; /* Require sane CRC. */
 
+    info->pulses_count = (off+10*8*2) - info->start_off;
+
     /* To convert the raw pressure to kPa, RTL433 uses 2.5, but is likely
      * wrong. Searching on Google for users experimenting with the value
      * reported, the value appears to be 2.75. */

+ 16 - 2
protocols/tpms/toyota.c

@@ -42,6 +42,7 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     for (j = 0; sync[j]; j++) {
         off = bitmap_seek_bits(bits,numbytes,0,numbits,sync[j]);
         if (off != BITMAP_SEEK_NOT_FOUND) {
+            info->start_off = off;
             off += strlen(sync[j])-2;
             break;
 	}
@@ -58,6 +59,19 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
     if (decoded < 8*9) return false; /* Require the full 8 bytes. */
     if (crc8(raw,8,0x80,7) != raw[8]) return false; /* Require sane CRC. */
 
+    /* We detected a valid signal. However now info->start_off is actually
+     * pointing to the sync part, not the preamble of alternating 0 and 1.
+     * Protoview decoders get called with some space to the left, in order
+     * for the decoder itself to fix the signal if neeeded, so that its
+     * logical representation will be more accurate and better to save
+     * and retransmit. */
+    if (info->start_off >= 12) {
+        info->start_off -= 12;
+        bitmap_set_pattern(bits,numbytes,info->start_off,"010101010101");
+    }
+
+    info->pulses_count = (off+8*9*2) - info->start_off;
+
     float kpa = (float)((raw[4]&0x7f)<<1 | raw[5]>>7) * 0.25 - 7;
     int temp = ((raw[5]&0x7f)<<1 | raw[6]>>7) - 40;
 
@@ -67,8 +81,8 @@ static bool decode(uint8_t *bits, uint32_t numbytes, uint32_t numbits, ProtoView
         raw[6],raw[7],raw[8]);
     snprintf(info->info1,sizeof(info->info1),"Tire ID %02X%02X%02X%02X",
         raw[0],raw[1],raw[2],raw[3]);
-    snprintf(info->info1,sizeof(info->info1),"Pressure %.2f psi", (double)kpa);
-    snprintf(info->info2,sizeof(info->info2),"Temperature %d C", temp);
+    snprintf(info->info2,sizeof(info->info2),"Pressure %.2f psi", (double)kpa);
+    snprintf(info->info3,sizeof(info->info3),"Temperature %d C", temp);
     return true;
 }
 

+ 140 - 12
signal.c

@@ -4,7 +4,6 @@
 #include "app.h"
 
 bool decode_signal(RawSamplesBuffer *s, uint64_t len, ProtoViewMsgInfo *info);
-void initialize_msg_info(ProtoViewMsgInfo *i);
 
 /* =============================================================================
  * Raw signal detection
@@ -23,6 +22,8 @@ void reset_current_signal(ProtoViewApp *app) {
     app->signal_decoded = false;
     raw_samples_reset(DetectedSamples);
     raw_samples_reset(RawSamples);
+    free_msg_info(app->msg_info);
+    app->msg_info = NULL;
 }
 
 /* This function starts scanning samples at offset idx looking for the
@@ -135,7 +136,6 @@ void scan_for_signal(ProtoViewApp *app) {
                                        than a few samples it's very easy to
                                        mistake noise for signal. */
 
-    ProtoViewMsgInfo *info = malloc(sizeof(ProtoViewMsgInfo));
     uint32_t i = 0;
 
     while (i < copy->total-1) {
@@ -143,10 +143,16 @@ void scan_for_signal(ProtoViewApp *app) {
 
         /* For messages that are long enough, attempt decoding. */
         if (thislen > minlen) {
-            initialize_msg_info(info);
+            /* Allocate the message information that some decoder may
+             * 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. */
+
             /* decode_signal() expects the detected signal to start
-             * from index .*/
+             * from index zero .*/
             raw_samples_center(copy,i);
             bool decoded = decode_signal(copy,thislen,info);
             copy->idx = saved_idx; /* Restore the index as we are scanning
@@ -158,7 +164,8 @@ void scan_for_signal(ProtoViewApp *app) {
             if ((thislen > app->signal_bestlen && app->signal_decoded == false)
                 || (app->signal_decoded == false && decoded))
             {
-                app->signal_info = *info;
+                free_msg_info(app->msg_info);
+                app->msg_info = info;
                 app->signal_bestlen = thislen;
                 app->signal_decoded = decoded;
                 raw_samples_copy(DetectedSamples,copy);
@@ -172,12 +179,15 @@ void scan_for_signal(ProtoViewApp *app) {
                     app->us_scale = 10;
                 else if (DetectedSamples->short_pulse_dur < 145)
                     app->us_scale = 30;
+            } else {
+                /* If the structure was not filled, discard it. Otherwise
+                 * now the owner is app->msg_info. */
+                free_msg_info(info);
             }
         }
         i += thislen ? thislen : 1;
     }
     raw_samples_free(copy);
-    free(info);
 }
 
 /* =============================================================================
@@ -215,6 +225,100 @@ bool bitmap_get(uint8_t *b, uint32_t blen, uint32_t bitpos) {
     return (b[byte] & (1<<bit)) != 0;
 }
 
+/* Copy 'count' bits from the bitmap 's' of 'slen' total bytes, to the
+ * bitmap 'd' of 'dlen' total bytes. The bits are copied starting from
+ * offset 'soff' of the source bitmap to the offset 'doff' of the
+ * destination bitmap. */
+void bitmap_copy(uint8_t *d, uint32_t dlen, uint32_t doff,
+                 uint8_t *s, uint32_t slen, uint32_t soff,
+                 uint32_t count)
+{
+    /* If we are byte-aligned in both source and destination, use a fast
+     * path for the number of bytes we can consume this way. */
+    if ((doff & 7) == 0 && (soff & 7) == 0) {
+        uint32_t didx = doff/8;
+        uint32_t sidx = soff/8;
+        while(count > 8 && didx < dlen && sidx < slen) {
+            d[didx++] = s[sidx++];
+            count -= 8;
+        }
+        doff = didx * 8;
+        soff = sidx * 8;
+        /* Note that if we entered this path, the count at the end
+         * of the loop will be < 8. */
+    }
+
+    /* Copy the bits needed to reach an offset where we can copy
+     * two half bytes of src to a full byte of destination. */
+    while(count > 8 && (doff&7) != 0) {
+        bool bit = bitmap_get(s,slen,soff++);
+        bitmap_set(d,dlen,doff++,bit);
+        count--;
+    }
+
+    /* If we are here and count > 8, we have an offset that is byte aligned
+     * to the destination bitmap, but not aligned to the source bitmap.
+     * We can copy fast enough by shifting each two bytes of the original
+     * bitmap.
+     *
+     * This is how it works:
+     *
+     *  dst:
+     *  +--------+--------+--------+
+     *  | 0      | 1      | 2      |
+     *  |        |        |        | <- data to fill
+     *  +--------+--------+--------+
+     *            ^
+     *            |
+     *            doff = 8
+     *
+     *  src:
+     *  +--------+--------+--------+
+     *  | 0      | 1      | 2      |
+     *  |hellowor|ld!HELLO|WORLDS!!| <- data to copy
+     *  +--------+--------+--------+
+     *               ^
+     *               |
+     *               soff = 11
+     *
+     *  skew = 11%8 = 3
+     *  each destination byte in dst will receive:
+     *
+     *  dst[doff/8] = (src[soff/8] << skew) | (src[soff/8+1] >> (8-skew))
+     *
+     *  dstbyte = doff/8 = 8/8 = 1
+     *  srcbyte = soff/8 = 11/8 = 1
+     *
+     *  so dst[1] will get:
+     *  src[1] << 3, that is "ld!HELLO" << 3 = "HELLO..."
+     *      xored with
+     *  src[2] << 5, that is "WORLDS!!" >> 5 = ".....WOR"
+     *  That is "HELLOWOR"
+     */
+    if (count > 8) {
+        uint8_t skew = soff % 8; /* Don't worry, compiler will optimize. */
+        uint32_t didx = doff/8;
+        uint32_t sidx = soff/8;
+        while(count > 8 && didx < dlen && sidx < slen) {
+            d[didx] = ((s[sidx] << skew) |
+                       (s[sidx+1] >> (8-skew)));
+            sidx++;
+            didx++;
+            soff += 8;
+            doff += 8;
+            count -= 8;
+        }
+    }
+
+    /* Here count is guaranteed to be < 8.
+     * Copy the final bits bit by bit. */
+    while(count) {
+        bool bit = bitmap_get(s,slen,soff++);
+        bitmap_set(d,dlen,doff++,bit);
+        count--;
+    }
+}
+
 /* We decode bits assuming the first bit we receive is the MSB
  * (see bitmap_set/get functions). Certain devices send data
  * encoded in the reverse way. */
@@ -259,15 +363,17 @@ uint32_t bitmap_seek_bits(uint8_t *b, uint32_t blen, uint32_t startpos, uint32_t
     return BITMAP_SEEK_NOT_FOUND;
 }
 
-/* Set the pattern 'pat' into the bitmap 'b' of max length 'blen' bytes.
+/* Set the pattern 'pat' into the bitmap 'b' of max length 'blen' bytes,
+ * starting from the specified offset.
+ *
  * The pattern is given as a string of 0s and 1s characters, like "01101001".
  * This function is useful in order to set the test vectors in the protocol
  * decoders, to see if the decoding works regardless of the fact we are able
  * to actually receive a given signal. */
-void bitmap_set_pattern(uint8_t *b, uint32_t blen, const char *pat) {
+void bitmap_set_pattern(uint8_t *b, uint32_t blen, uint32_t off, const char *pat) {
     uint32_t i = 0;
     while(pat[i]) {
-        bitmap_set(b,blen,i,pat[i] == '1');
+        bitmap_set(b,blen,i+off,pat[i] == '1');
         i++;
     }
 }
@@ -408,10 +514,19 @@ ProtoViewDecoder *Decoders[] = {
     NULL
 };
 
+/* Free the message info and allocated data. */
+void free_msg_info(ProtoViewMsgInfo *i) {
+    if (i == NULL) return;
+    free(i->bits);
+    free(i);
+}
+
 /* Reset the message info structure before passing it to the decoding
  * functions. */
-void initialize_msg_info(ProtoViewMsgInfo *i) {
+void init_msg_info(ProtoViewMsgInfo *i, ProtoViewApp *app) {
+    UNUSED(app);
     memset(i,0,sizeof(ProtoViewMsgInfo));
+    i->bits = NULL;
 }
 
 /* This function is called when a new signal is detected. It converts it
@@ -424,7 +539,7 @@ bool decode_signal(RawSamplesBuffer *s, uint64_t len, ProtoViewMsgInfo *info) {
 
     /* We call the decoders with an offset a few samples before the actual
      * signal detected and for a len of a few bits after its end. */
-    uint32_t before_samples = 20;
+    uint32_t before_samples = 32;
     uint32_t after_samples = 100;
 
     uint8_t *bitmap = malloc(bitmap_size);
@@ -458,7 +573,20 @@ bool decode_signal(RawSamplesBuffer *s, uint64_t len, ProtoViewMsgInfo *info) {
     if (!decoded) {
         FURI_LOG_E(TAG, "No decoding possible");
     } else {
-        FURI_LOG_E(TAG, "Decoded %s, raw=%s info=[%s,%s,%s,%s]", info->name, info->raw, info->info1, info->info2, info->info3, info->info4);
+        FURI_LOG_E(TAG, "Decoded %s, raw=%s info=[%s,%s,%s,%s]",
+            info->name, info->raw, info->info1, info->info2,
+            info->info3, info->info4);
+        /* The message was correctly decoded: fill the info structure
+         * with the decoded signal. The decoder may not implement offset/len
+         * filling of the structure. In such case we have no info and
+         * pulses_count will be set to zero. */
+        if (info->pulses_count) {
+            info->bits_bytes = (info->pulses_count+7)/8; // Round to full byte.
+            info->bits = malloc(info->bits_bytes);
+            bitmap_copy(info->bits,info->bits_bytes,0,
+                        bitmap,bitmap_size,info->start_off,
+                        info->pulses_count);
+        }
     }
     free(bitmap);
     return decoded;

+ 141 - 0
signal_file.c

@@ -0,0 +1,141 @@
+/* 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 <stream/stream.h>
+#include <flipper_format/flipper_format_i.h>
+
+/* ========================= 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;
+        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);
+            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;
+}

+ 74 - 0
ui.c

@@ -3,6 +3,80 @@
 
 #include "app.h"
 
+/* =========================== Subview handling ================================
+ * Note that these are not the Flipper subviews, but the subview system
+ * implemented inside ProtoView.
+ * ========================================================================== */
+
+/* Return the ID of the currently selected subview, of the current
+ * view. */
+int get_current_subview(ProtoViewApp *app) {
+    return app->current_subview[app->current_view];
+}
+
+/* Called by view rendering callback that has subviews, to show small triangles
+ * facing down/up if there are other subviews the user can access with up
+ * and down. */
+void show_available_subviews(Canvas *canvas, ProtoViewApp *app,
+                             int last_subview)
+{
+    int subview = get_current_subview(app);
+    if (subview != 0)
+        canvas_draw_triangle(canvas,120,5,8,5,CanvasDirectionBottomToTop);
+    if (subview != last_subview-1)
+        canvas_draw_triangle(canvas,120,59,8,5,CanvasDirectionTopToBottom);
+}
+
+/* Handle up/down keys when we are in a subview. If the function catched
+ * such keypress, it returns true, so that the actual view input callback
+ * knows it can just return ASAP without doing anything. */
+bool process_subview_updown(ProtoViewApp *app, InputEvent input, int last_subview) {
+    int subview = get_current_subview(app);
+    if (input.type == InputTypePress) {
+        if (input.key == InputKeyUp) {
+            if (subview != 0)
+                app->current_subview[app->current_view]--;
+            return true;
+        } else if (input.key == InputKeyDown) {
+            if (subview != last_subview-1)
+                app->current_subview[app->current_view]++;
+            return true;
+        }
+    }
+    return false;
+}
+
+/* ============================= Text input ====================================
+ * Normally we just use our own private UI widgets. However for the text input
+ * widget, that is quite complex, visualizes a keyboard and must be standardized
+ * for user coherent experience, we use the one provided by the Flipper
+ * framework. The following two functions allow to show the keyboard to get
+ * text and later dismiss it.
+ * ========================================================================== */
+
+/* Show the keyboard, take the user input and store it into the specified
+ * 'buffer' of 'buflen' total bytes. When the user is done, the done_callback
+ * is called passing the application context to it. Such callback needs
+ * to do whatever it wants with the input buffer and dismissi the keyboard
+ * calling: dismiss_keyboard(app);
+ *
+ * Note: if the buffer is not a null-termined zero string, what it contains will
+ * be used as initial input for the user. */
+void show_keyboard(ProtoViewApp *app, char *buffer, uint32_t buflen,
+                   void (*done_callback)(void*))
+{
+    app->show_text_input = true;
+    app->text_input_buffer = buffer;
+    app->text_input_buffer_len = buflen;
+    app->text_input_done_callback = done_callback;
+}
+
+void dismiss_keyboard(ProtoViewApp *app) {
+    view_dispatcher_stop(app->view_dispatcher);
+}
+
+/* =========================== Canvas extensions ============================ */
+
 void canvas_draw_str_with_border(Canvas* canvas, uint8_t x, uint8_t y, const char* str, Color text_color, Color border_color)
 {
     struct {

+ 139 - 16
view_info.c

@@ -2,41 +2,164 @@
  * See the LICENSE file for information about the license. */
 
 #include "app.h"
+#include <gui/view_i.h>
+#include <lib/toolbox/random_name.h>
 
-/* Renders the view with the detected message information. */
-void render_view_info(Canvas *const canvas, ProtoViewApp *app) {
-    if (app->signal_decoded == false) {
-        canvas_set_font(canvas, FontSecondary);
-        canvas_draw_str(canvas, 30,36,"No signal decoded");
-        return;
-    }
+enum {
+    SubViewInfoMain,
+    SubViewInfoSave,
+    SubViewInfoLast, /* Just a sentinel. */
+};
+
+/* Our view private data. */
+#define SAVE_FILENAME_LEN 64
+typedef struct {
+    /* Our save view displays an oscilloscope-alike resampled signal,
+     * so that the user can see what they are saving. With left/right
+     * you can move to next rows. Here we store where we are. */
+    uint32_t signal_display_start_row;
+    char *filename;
+} InfoViewPrivData;
 
+/* Render the view with the detected message information. */
+static void render_subview_main(Canvas *const canvas, ProtoViewApp *app) {
     /* Protocol name as title. */
     canvas_set_font(canvas, FontPrimary);
     uint8_t y = 8, lineheight = 10;
-    canvas_draw_str(canvas, 0, y, app->signal_info.name);
+    canvas_draw_str(canvas, 0, y, app->msg_info->name);
     y += lineheight;
 
     /* Info fields. */
     char buf[128];
     canvas_set_font(canvas, FontSecondary);
-    if (app->signal_info.raw[0]) {
-        snprintf(buf,sizeof(buf),"Raw: %s", app->signal_info.raw);
+    if (app->msg_info->raw[0]) {
+        snprintf(buf,sizeof(buf),"Raw: %s", app->msg_info->raw);
         canvas_draw_str(canvas, 0, y, buf);
         y += lineheight;
     }
-    canvas_draw_str(canvas, 0, y, app->signal_info.info1); y += lineheight;
-    canvas_draw_str(canvas, 0, y, app->signal_info.info2); y += lineheight;
-    canvas_draw_str(canvas, 0, y, app->signal_info.info3); y += lineheight;
-    canvas_draw_str(canvas, 0, y, app->signal_info.info4); y += lineheight;
+    canvas_draw_str(canvas, 0, y, app->msg_info->info1); y += lineheight;
+    canvas_draw_str(canvas, 0, y, app->msg_info->info2); y += lineheight;
+    canvas_draw_str(canvas, 0, y, app->msg_info->info3); y += lineheight;
+    canvas_draw_str(canvas, 0, y, app->msg_info->info4); y += lineheight;
+
+    y = 37;
+    lineheight = 7;
+    canvas_draw_str(canvas, 119, y, "s"); y += lineheight;
+    canvas_draw_str(canvas, 119, y, "a"); y += lineheight;
+    canvas_draw_str(canvas, 119, y, "v"); y += lineheight;
+    canvas_draw_str(canvas, 119, y, "e"); y += lineheight;
+}
+
+/* Render view with save option. */
+static void render_subview_save(Canvas *const canvas, ProtoViewApp *app) {
+    InfoViewPrivData *privdata = app->view_privdata;
+
+    /* Display our signal in digital form: here we don't show the
+     * signal with the exact timing of the received samples, but as it
+     * is in its logic form, in exact multiples of the short pulse length. */
+    uint8_t rows = 6;
+    uint8_t rowheight = 11;
+    uint8_t bitwidth = 4;
+    uint8_t bitheight = 5;
+    uint32_t idx = privdata->signal_display_start_row * (128/4);
+    bool prevbit = false;
+    for (uint8_t y = bitheight+12; y <= rows*rowheight; y += rowheight) {
+        for (uint8_t x = 0; x < 128; x += 4) {
+            bool bit = bitmap_get(app->msg_info->bits,
+                                  app->msg_info->bits_bytes,idx);
+            uint8_t prevy = y + prevbit*(bitheight*-1) - 1;
+            uint8_t thisy = y + bit*(bitheight*-1) - 1;
+            canvas_draw_line(canvas,x,prevy,x,thisy);
+            canvas_draw_line(canvas,x,thisy,x+bitwidth-1,thisy);
+            prevbit = bit;
+            if (idx >= app->msg_info->pulses_count) {
+                canvas_set_color(canvas, ColorWhite);
+                canvas_draw_dot(canvas, x+1,thisy);
+                canvas_draw_dot(canvas, x+3,thisy);
+                canvas_set_color(canvas, ColorBlack);
+            }
+            idx++; // Draw next bit
+        }
+    }
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 6, "ok: save, < >: slide rows");
+}
+
+/* Render the selected subview of this view. */
+void render_view_info(Canvas *const canvas, ProtoViewApp *app) {
+    if (app->signal_decoded == false) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 30,36,"No signal decoded");
+        return;
+    }
+
+    show_available_subviews(canvas,app,SubViewInfoLast);
+    switch(app->current_subview[app->current_view]) {
+    case SubViewInfoMain: render_subview_main(canvas,app); break;
+    case SubViewInfoSave: render_subview_save(canvas,app); break;
+    }
+}
+
+/* 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);
+}
+
+/* Replace all the occurrences of character c1 with c2 in the specified
+ * string. */
+void str_replace(char *buf, char c1, char c2) {
+    char *p = buf;
+    while(*p) {
+        if (*p == c1) *p = c2;
+        p++;
+    }
+}
+
+/* Set a random filename the user can edit. */
+void set_signal_random_filename(ProtoViewApp *app, char *buf, size_t buflen) {
+    char suffix[6];
+    set_random_name(suffix,sizeof(suffix));
+    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. */
 void process_input_info(ProtoViewApp *app, InputEvent input) {
-    if (input.type == InputTypeShort) {
-        if (input.key == InputKeyOk) {
+    if (process_subview_updown(app,input,SubViewInfoLast)) return;
+    InfoViewPrivData *privdata = app->view_privdata;
+    int subview = get_current_subview(app);
+
+    /* Main subview. */
+    if (subview == SubViewInfoMain) {
+        if (input.type == InputTypeShort && input.key == InputKeyOk) {
             /* Reset the current sample to capture the next. */
             reset_current_signal(app);
         }
+    } else if (subview == SubViewInfoSave) {
+    /* Save subview. */
+        if (input.type == InputTypePress && input.key == InputKeyRight) {
+            privdata->signal_display_start_row++;
+        } else if (input.type == InputTypePress && input.key == InputKeyLeft) {
+            if (privdata->signal_display_start_row != 0)
+                privdata->signal_display_start_row--;
+        } else if (input.type == InputTypePress && input.key == InputKeyOk) {
+            privdata->filename = malloc(SAVE_FILENAME_LEN);
+            set_signal_random_filename(app,privdata->filename,SAVE_FILENAME_LEN);
+            show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN,
+                          text_input_done_callback);
+        }
     }
 }

+ 1 - 1
view_raw_signal.c

@@ -65,7 +65,7 @@ void render_view_raw_pulses(Canvas *const canvas, ProtoViewApp *app) {
     canvas_draw_str_with_border(canvas, 97, 63, buf, ColorWhite, ColorBlack);
     if (app->signal_decoded) {
         canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_with_border(canvas, 1, 61, app->signal_info.name, ColorWhite, ColorBlack);
+        canvas_draw_str_with_border(canvas, 1, 61, app->msg_info->name, ColorWhite, ColorBlack);
     }
 }