Переглянути джерело

Initial commit with working minimal app.

antirez 3 роки тому
коміт
4f8f7d8ca5
10 змінених файлів з 657 додано та 0 видалено
  1. 24 0
      LICENSE
  2. 20 0
      TODO
  3. 285 0
      app.c
  4. 54 0
      app.h
  5. 63 0
      app_buffer.c
  6. 26 0
      app_buffer.h
  7. 63 0
      app_subghz.c
  8. BIN
      appicon.png
  9. 12 0
      application.fam
  10. 110 0
      proto.c

+ 24 - 0
LICENSE

@@ -0,0 +1,24 @@
+Copyright (c) 2022-2023 Salvatore Sanfilippo <antirez at gmail dot com>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 20 - 0
TODO

@@ -0,0 +1,20 @@
+Core improvements
+=================
+
+- Remove processing from rendering callback, put it into timer.
+- Detection of non Manchester and non RZ encoded signals. Not sure if there are any signals that are not self clocked widely used in RF.
+
+Features
+========
+
+- In raw visualization mode, up/down change scaling (100,200,...,500us per pixel)
+- Pressing right/left you browse different modes:
+  * Current best signal pulse classes.
+  * Raw square wave display. Central button freezes and resumes (toggle). When frozen we display "paused" (inverted) on the low part of the screen.
+- Setup of frequency, AM/FM, ... done with single screens (for each function) found on the right, so that when you are on that screen up/down has the effect of changing frequency / mode.
+
+Screens sequence (user can navigate with <- and ->):
+
+
+                            (default)
+[modulation] <> [freq] <> [pulses view] <> [raw square view] <> [signal info]

+ 285 - 0
app.c

@@ -0,0 +1,285 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <lib/flipper_format/flipper_format.h>
+#include <input/input.h>
+#include <gui/gui.h>
+#include <stdlib.h>
+#include "app.h"
+#include "app_buffer.h"
+
+#define FREQ 433920000
+
+RawSamplesBuffer *RawSamples, *DetectedSamples;
+extern const SubGhzProtocolRegistry protoview_protocol_registry;
+
+/* Render the received signal.
+ *
+ * The screen of the flipper is 128 x 64. Even using 4 pixels per line
+ * (where low level signal is one pixel high, high level is 4 pixels
+ * high) and 4 pixels of spacing between the different lines, we can
+ * plot comfortably 8 lines.
+ *
+ * The 'idx' argument is the first sample to render in the circular
+ * buffer. */
+void render_signal(Canvas *const canvas, RawSamplesBuffer *buf, uint32_t idx) {
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, 0, 0, 127, 63);
+    canvas_set_color(canvas, ColorBlack);
+
+    int rows = 8;
+    uint32_t time_per_pixel = 100;
+    bool level = 0;
+    uint32_t dur = 0;
+    for (int row = 0; row < rows ; row++) {
+        for (int x = 0; x < 128; x++) {
+            int y = 3 + row*8;
+            if (dur < time_per_pixel/2) {
+                /* Get more data. */
+                raw_samples_get(buf, idx++, &level, &dur);
+            }
+
+            canvas_draw_line(canvas, x,y,x,y-(level*3));
+
+            /* Remove from the current level duration the time we
+             * just plot. */
+            if (dur > time_per_pixel)
+                dur -= time_per_pixel;
+            else
+                dur = 0;
+        }
+    }
+}
+
+/* Return the time difference between a and b, always >= 0 since
+ * the absolute value is returned. */
+uint32_t duration_delta(uint32_t a, uint32_t b) {
+    return a > b ? a - b : b -a;
+}
+
+/* This function starts scanning samples at offset idx looking for the
+ * longest run of pulses, either high or low, that are among 10%
+ * of each other, for a maximum of three classes. The classes are
+ * counted separtely for high and low signals (RF on / off) because
+ * many devices tend to have different pulse lenghts depending on
+ * the level of the pulse.
+ *
+ * For instance Oregon2 sensors, in the case of protocol 2.1 will send
+ * pulses of ~400us (RF on) VS ~580us (RF off). */
+#define SEARCH_CLASSES 3
+uint32_t search_coherent_signal(RawSamplesBuffer *s, uint32_t idx) {
+    struct {
+        uint32_t dur[2];     /* dur[0] = low, dur[1] = high */
+        uint32_t count[2];   /* Associated observed frequency. */
+    } classes[SEARCH_CLASSES];
+
+    memset(classes,0,sizeof(classes));
+    uint32_t minlen = 80, maxlen = 4000; /* Depends on data rate, here we
+                                            allow for high and low. */
+    uint32_t len = 0; /* Observed len of coherent samples. */
+    s->short_pulse_dur = 0;
+    for (uint32_t j = idx; j < idx+100; j++) {
+        bool level;
+        uint32_t dur;
+        raw_samples_get(s, j, &level, &dur);
+        if (dur < minlen || dur > maxlen) return len;
+
+        /* Let's see if it matches a class we already have or if we
+         * can populate a new (yet empty) class. */
+        uint32_t k;
+        for (k = 0; k < SEARCH_CLASSES; k++) {
+            if (classes[k].count[level] == 0) {
+                classes[k].dur[level] = dur;
+                classes[k].count[level] = 1;
+                break;
+            } else {
+                uint32_t classavg = classes[k].dur[level];
+                uint32_t count = classes[k].count[level];
+                uint32_t delta = duration_delta(dur,classavg);
+                if (delta < classavg/10) {
+                    /* It is useful to compute the average of the class
+                     * we are observing. We know how many samples we got so
+                     * far, so we can recompute the average easily.
+                     * By always having a better estimate of the pulse len
+                     * we can avoid missing next samples in case the first
+                     * observed samples are too off. */
+                    classavg = ((classavg * count) + dur) / (count+1);
+                    classes[k].dur[level] = classavg;
+                    classes[k].count[level]++;
+                    break;
+                }
+            }
+        }
+
+        if (k == SEARCH_CLASSES) { /* No match, return. */
+            return len;
+        } else {
+            /* Update the buffer setting the shortest pulse we found
+             * among the three classes. This will be used when scaling
+             * for visualization. */
+            if (s->short_pulse_dur == 0 || dur < s->short_pulse_dur)
+                s->short_pulse_dur = dur;
+        }
+        len++;
+    }
+    return len;
+}
+
+/* Search the buffer with the stored signal (last N samples received)
+ * in order to find a coherent signal. If a signal that does not appear to
+ * be just noise is found, it is set in DetectedSamples global signal
+ * buffer, that is what is rendered on the screen. */
+void scan_for_signal(ProtoViewApp *app) {
+    /* We need to work on a copy: the RawSamples buffer is populated
+     * by the background thread receiving data. */
+    RawSamplesBuffer *copy = raw_samples_alloc();
+    raw_samples_copy(copy,RawSamples);
+
+    /* Try to seek on data that looks to have a regular high low high low
+     * pattern. */
+    uint32_t minlen = 10;           /* Min run of coherent samples. */
+
+    for (uint32_t i = 0; i < copy->total-1; i++) {
+        uint32_t thislen = search_coherent_signal(copy,i);
+        if (thislen > minlen && thislen > app->signal_bestlen) {
+            app->signal_bestlen = thislen;
+            raw_samples_copy(DetectedSamples,copy);
+            DetectedSamples->idx = (DetectedSamples->idx+i)%
+                                   DetectedSamples->total;
+            FURI_LOG_E(TAG, "Displayed sample updated");
+        }
+    }
+    raw_samples_free(copy);
+}
+
+static void render_callback(Canvas *const canvas, void *ctx) {
+    ProtoViewApp *app = ctx;
+    scan_for_signal(app);
+    render_signal(canvas, DetectedSamples, 0);
+}
+
+/* Here all we do is putting the events into the queue that will be handled
+ * in the while() loop of the app entry point function. */
+static void input_callback(InputEvent* input_event, void* ctx)
+{
+    ProtoViewApp *app = ctx;
+
+    if (input_event->type == InputTypePress) {
+        furi_message_queue_put(app->event_queue,input_event,FuriWaitForever);
+        FURI_LOG_E(TAG, "INPUT CALLBACK %d", (int)input_event->key);
+    }
+}
+
+ProtoViewApp* protoview_app_alloc() {
+    ProtoViewApp *app = malloc(sizeof(ProtoViewApp));
+
+    // Init shared data structures
+    RawSamples = raw_samples_alloc();
+    DetectedSamples = raw_samples_alloc();
+
+    //init setting
+    app->setting = subghz_setting_alloc();
+    subghz_setting_load(app->setting, EXT_PATH("protoview/settings.txt"));
+
+    // GUI
+    app->gui = furi_record_open(RECORD_GUI);
+    app->view_port = view_port_alloc();
+    view_port_draw_callback_set(app->view_port, render_callback, app);
+    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));
+
+    // Signal found
+    app->signal_bestlen = 0;
+
+    //init Worker & Protocol
+    app->txrx = malloc(sizeof(ProtoViewTxRx));
+    app->txrx->preset = malloc(sizeof(SubGhzRadioPreset));
+    app->txrx->preset->name = furi_string_alloc();
+
+    /* Setup rx worker and environment. */
+    app->txrx->worker = subghz_worker_alloc();
+    app->txrx->environment = subghz_environment_alloc();
+    subghz_environment_set_protocol_registry(
+        app->txrx->environment, (void*)&protoview_protocol_registry);
+    app->txrx->receiver = subghz_receiver_alloc_init(app->txrx->environment);
+
+    subghz_receiver_set_filter(app->txrx->receiver, SubGhzProtocolFlag_Decodable);
+    subghz_worker_set_overrun_callback(
+        app->txrx->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
+    subghz_worker_set_pair_callback(
+        app->txrx->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
+    subghz_worker_set_context(app->txrx->worker, app->txrx->receiver);
+
+    furi_hal_power_suppress_charge_enter();
+    app->running = 1;
+
+    return app;
+}
+
+void protoview_app_free(ProtoViewApp *app) {
+    furi_assert(app);
+
+    //CC1101 off
+    radio_sleep(app);
+
+    // View
+    view_port_enabled_set(app->view_port, false);
+    gui_remove_view_port(app->gui, app->view_port);
+    view_port_free(app->view_port);
+    furi_record_close(RECORD_GUI);
+    furi_message_queue_free(app->event_queue);
+    app->gui = NULL;
+
+    //setting
+    subghz_setting_free(app->setting);
+
+    //Worker
+    subghz_receiver_free(app->txrx->receiver);
+    subghz_environment_free(app->txrx->environment);
+    subghz_worker_free(app->txrx->worker);
+    furi_string_free(app->txrx->preset->name);
+    free(app->txrx->preset);
+    free(app->txrx);
+
+    furi_hal_power_suppress_charge_exit();
+    raw_samples_free(RawSamples);
+    raw_samples_free(DetectedSamples);
+
+    free(app);
+}
+
+int32_t protoview_app_entry(void* p) {
+    UNUSED(p);
+    ProtoViewApp *app = protoview_app_alloc();
+
+    radio_begin(app);
+    radio_rx(app, FREQ);
+
+    InputEvent input;
+    while(app->running) {
+        FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100);
+        if (qstat == FuriStatusOk) {
+            if (input.key == InputKeyBack) {
+                app->running = 0;
+            } else if (input.key == InputKeyOk) {
+                app->signal_bestlen = 0;
+                raw_samples_reset(DetectedSamples);
+            }
+            FURI_LOG_E(TAG, "Main Loop - Input: %u", input.key);
+        } else {
+            static int c = 0;
+            c++;
+            if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
+        }
+        view_port_update(app->view_port);
+    }
+
+    if (app->txrx->txrx_state == TxRxStateRx) {
+        FURI_LOG_E(TAG, "Putting CC1101 to sleep before exiting.");
+        radio_rx_end(app);
+        radio_sleep(app);
+    }
+
+    protoview_app_free(app);
+    return 0;
+}

+ 54 - 0
app.h

@@ -0,0 +1,54 @@
+#pragma once
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <notification/notification_messages.h>
+#include <lib/subghz/subghz_setting.h>
+#include <lib/subghz/subghz_worker.h>
+#include <lib/subghz/receiver.h>
+#include <lib/subghz/transmitter.h>
+#include <lib/subghz/registry.h>
+
+#define TAG "ProtoView"
+
+typedef struct ProtoViewApp ProtoViewApp;
+
+/* Subghz system state */
+typedef enum {
+    TxRxStateIDLE,
+    TxRxStateRx,
+    TxRxStateSleep,
+} TxRxState;
+
+/* This is the context of our subghz worker and associated thread.
+ * It receives data and we get our protocol "feed" callback called
+ * with the level (1 or 0) and duration. */
+struct ProtoViewTxRx {
+    SubGhzWorker* worker;
+    SubGhzEnvironment* environment;
+    SubGhzReceiver* receiver;
+    SubGhzRadioPreset* preset;
+    TxRxState txrx_state; /* Receiving, idle or sleeping? */
+};
+
+typedef struct ProtoViewTxRx ProtoViewTxRx;
+
+struct ProtoViewApp {
+    Gui *gui;
+    ViewPort *view_port;
+    FuriMessageQueue *event_queue;
+    ProtoViewTxRx *txrx;     /* Radio state. */
+    SubGhzSetting *setting;
+    int running;             /* Once false exists the app. */
+    uint32_t signal_bestlen; /* Longest coherent signal observed so far. */
+};
+
+void radio_begin(ProtoViewApp* app);
+uint32_t radio_rx(ProtoViewApp* app, uint32_t frequency);
+void radio_idle(ProtoViewApp* app);
+void radio_rx_end(ProtoViewApp* app);
+void radio_sleep(ProtoViewApp* app);

+ 63 - 0
app_buffer.c

@@ -0,0 +1,63 @@
+#include <inttypes.h>
+#include <furi/core/string.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include "app_buffer.h"
+
+/* Allocate and initialize a samples buffer. */
+RawSamplesBuffer *raw_samples_alloc(void) {
+    RawSamplesBuffer *buf = malloc(sizeof(*buf));
+    buf->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    raw_samples_reset(buf);
+    return buf;
+}
+
+/* Free a sample buffer. Should be called when the mutex is released. */
+void raw_samples_free(RawSamplesBuffer *s) {
+    furi_mutex_free(s->mutex);
+    free(s);
+}
+
+/* This just set all the samples to zero and also resets the internal
+ * index. There is no need to call it after raw_samples_alloc(), but only
+ * when one wants to reset the whole buffer of samples. */
+void raw_samples_reset(RawSamplesBuffer *s) {
+    furi_mutex_acquire(s->mutex,FuriWaitForever);
+    s->total = RAW_SAMPLES_NUM;
+    s->idx = 0;
+    s->short_pulse_dur = 0;
+    memset(s->level,0,sizeof(s->level));
+    memset(s->dur,0,sizeof(s->dur));
+    furi_mutex_release(s->mutex);
+}
+
+/* Add the specified sample in the circular buffer. */
+void raw_samples_add(RawSamplesBuffer *s, bool level, uint32_t dur) {
+    furi_mutex_acquire(s->mutex,FuriWaitForever);
+    s->level[s->idx] = level;
+    s->dur[s->idx] = dur;
+    s->idx = (s->idx+1) % RAW_SAMPLES_NUM;
+    furi_mutex_release(s->mutex);
+}
+
+/* Get the sample from the buffer. It is possible to use out of range indexes
+ * as 'idx' because the modulo operation will rewind back from the start. */
+void raw_samples_get(RawSamplesBuffer *s, uint32_t idx, bool *level, uint32_t *dur)
+{
+    furi_mutex_acquire(s->mutex,FuriWaitForever);
+    idx = (s->idx + idx) % RAW_SAMPLES_NUM;
+    *level = s->level[idx];
+    *dur = s->dur[idx];
+    furi_mutex_release(s->mutex);
+}
+
+/* Copy one buffer to the other, including current index. */
+void raw_samples_copy(RawSamplesBuffer *dst, RawSamplesBuffer *src) {
+    furi_mutex_acquire(src->mutex,FuriWaitForever);
+    furi_mutex_acquire(dst->mutex,FuriWaitForever);
+    dst->idx = src->idx;
+    memcpy(dst->level,src->level,sizeof(dst->level));
+    memcpy(dst->dur,src->dur,sizeof(dst->dur));
+    furi_mutex_release(src->mutex);
+    furi_mutex_release(dst->mutex);
+}

+ 26 - 0
app_buffer.h

@@ -0,0 +1,26 @@
+/* Our circular buffer of raw samples, used in order to display
+ * the signal. */
+
+#define RAW_SAMPLES_NUM 1024 /* Use a power of two: we take the modulo
+                                of the index quite often to normalize inside
+                                the range, and division is slow. */
+
+typedef struct RawSamplesBuffer {
+    FuriMutex *mutex;
+    uint8_t level[RAW_SAMPLES_NUM];
+    uint32_t dur[RAW_SAMPLES_NUM];
+    uint32_t idx;   /* Current idx (next to write). */
+    uint32_t total; /* Total samples: same as RAW_SAMPLES_NUM, we provide
+                       this field for a cleaner interface with the user, but
+                       we always use RAW_SAMPLES_NUM when taking the modulo so
+                       the compiler can optimize % as bit masking. */
+    /* Signal features. */
+    uint32_t short_pulse_dur; /* Duration of the shortest pulse. */
+} RawSamplesBuffer;
+
+RawSamplesBuffer *raw_samples_alloc(void);
+void raw_samples_reset(RawSamplesBuffer *s);
+void raw_samples_add(RawSamplesBuffer *s, bool level, uint32_t dur);
+void raw_samples_get(RawSamplesBuffer *s, uint32_t idx, bool *level, uint32_t *dur);
+void raw_samples_copy(RawSamplesBuffer *dst, RawSamplesBuffer *src);
+void raw_samples_free(RawSamplesBuffer *s);

+ 63 - 0
app_subghz.c

@@ -0,0 +1,63 @@
+#include "app.h"
+
+#include <flipper_format/flipper_format_i.h>
+
+/* Called after the application initialization in order to setup the
+ * subghz system and put it into idle state. If the user wants to start
+ * receiving we will call radio_rx() to start a receiving worker and
+ * associated thread. */
+void radio_begin(ProtoViewApp* app) {
+    furi_assert(app);
+    furi_hal_subghz_reset();
+    furi_hal_subghz_idle();
+    furi_hal_subghz_load_preset(FuriHalSubGhzPresetOok650Async);
+    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
+    app->txrx->txrx_state = TxRxStateIDLE;
+}
+
+/* Setup subghz to start receiving using a background worker. */
+uint32_t radio_rx(ProtoViewApp* app, uint32_t frequency) {
+    furi_assert(app);
+    if(!furi_hal_subghz_is_frequency_valid(frequency)) {
+        furi_crash(TAG" Incorrect RX frequency.");
+    }
+
+    if (app->txrx->txrx_state == TxRxStateRx) return frequency;
+
+    furi_hal_subghz_idle(); /* Put it into idle state in case it is sleeping. */
+    uint32_t value = furi_hal_subghz_set_frequency_and_path(frequency);
+    FURI_LOG_E(TAG, "Switched to frequency: %lu", value);
+    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
+    furi_hal_subghz_flush_rx();
+    furi_hal_subghz_rx();
+
+    furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, app->txrx->worker);
+    subghz_worker_start(app->txrx->worker);
+    app->txrx->txrx_state = TxRxStateRx;
+    return value;
+}
+
+/* Stop subghz worker (if active), put radio on idle state. */
+void radio_rx_end(ProtoViewApp* app) {
+    furi_assert(app);
+    if (app->txrx->txrx_state == TxRxStateRx) {
+        if(subghz_worker_is_running(app->txrx->worker)) {
+            subghz_worker_stop(app->txrx->worker);
+            furi_hal_subghz_stop_async_rx();
+        }
+    }
+    furi_hal_subghz_idle();
+    app->txrx->txrx_state = TxRxStateIDLE;
+}
+
+/* Put radio on sleep. */
+void radio_sleep(ProtoViewApp* app) {
+    furi_assert(app);
+    if (app->txrx->txrx_state == TxRxStateRx) {
+        /* We can't go from having an active RX worker to sleeping.
+         * Stop the RX subsystems first. */
+        radio_rx_end(app);
+    }
+    furi_hal_subghz_sleep();
+    app->txrx->txrx_state = TxRxStateSleep;
+}


+ 12 - 0
application.fam

@@ -0,0 +1,12 @@
+App(
+    appid="protoview",
+    name="Protocols visualizer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="protoview_app_entry",
+    cdefines=["APP_PROTOVIEW"],
+    requires=["gui"],
+    stack_size=8 * 1024,
+    order=50,
+    fap_icon="appicon.png",
+    fap_category="Tools",
+)

+ 110 - 0
proto.c

@@ -0,0 +1,110 @@
+#include <inttypes.h>
+#include <lib/flipper_format/flipper_format_i.h>
+#include <furi/core/string.h>
+#include <lib/subghz/registry.h>
+#include <lib/subghz/protocols/base.h>
+#include "app_buffer.h"
+
+#define TAG "PROTOVIEW-protocol"
+
+const SubGhzProtocol subghz_protocol_protoview;
+
+/* The feed() method puts data in the RawSamples global (protected by
+ * a mutex). */
+extern RawSamplesBuffer *RawSamples;
+
+/* This is totally dummy: we just define the decoder base for the async
+ * system to work but we don't really use it if not to collect raw
+ * data via the feed() method. */
+typedef struct SubGhzProtocolDecoderprotoview {
+    SubGhzProtocolDecoderBase base;
+} SubGhzProtocolDecoderprotoview;
+
+void* subghz_protocol_decoder_protoview_alloc(SubGhzEnvironment* environment) {
+    UNUSED(environment);
+
+    SubGhzProtocolDecoderprotoview* instance =
+        malloc(sizeof(SubGhzProtocolDecoderprotoview));
+    instance->base.protocol = &subghz_protocol_protoview;
+    return instance;
+}
+
+void subghz_protocol_decoder_protoview_free(void* context) {
+    furi_assert(context);
+    SubGhzProtocolDecoderprotoview* instance = context;
+    free(instance);
+}
+
+void subghz_protocol_decoder_protoview_reset(void* context) {
+    furi_assert(context);
+}
+
+/* That's the only thig we really use of the protocol decoder
+ * implementation. */
+void subghz_protocol_decoder_protoview_feed(void* context, bool level, uint32_t duration) {
+    furi_assert(context);
+    UNUSED(context);
+
+    /* Add data to the circular buffer. */
+    raw_samples_add(RawSamples, level, duration);
+    // FURI_LOG_E(TAG, "FEED: %d %d", (int)level, (int)duration);
+    return;
+}
+
+/* The only scope of this method is to avoid duplicated messages in the
+ * Subghz history, which we don't use. */
+uint8_t subghz_protocol_decoder_protoview_get_hash_data(void* context) {
+    furi_assert(context);
+    return 123;
+}
+
+bool subghz_protocol_decoder_protoview_serialize(
+    void* context,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset)
+{
+    UNUSED(context);
+    UNUSED(flipper_format);
+    UNUSED(preset);
+    return false;
+}
+
+bool subghz_protocol_decoder_protoview_deserialize(void* context, FlipperFormat* flipper_format)
+{
+    UNUSED(context);
+    UNUSED(flipper_format);
+    return false;
+}
+
+void subhz_protocol_decoder_protoview_get_string(void* context, FuriString* output)
+{
+    furi_assert(context);
+    furi_string_cat_printf(output, "Protoview");
+}
+
+const SubGhzProtocolDecoder subghz_protocol_protoview_decoder = {
+    .alloc = subghz_protocol_decoder_protoview_alloc,
+    .free = subghz_protocol_decoder_protoview_free,
+    .reset = subghz_protocol_decoder_protoview_reset,
+    .feed = subghz_protocol_decoder_protoview_feed,
+    .get_hash_data = subghz_protocol_decoder_protoview_get_hash_data,
+    .serialize = subghz_protocol_decoder_protoview_serialize,
+    .deserialize = subghz_protocol_decoder_protoview_deserialize,
+    .get_string = subhz_protocol_decoder_protoview_get_string,
+};
+
+const SubGhzProtocol subghz_protocol_protoview = {
+    .name = "Protoview",
+    .type = SubGhzProtocolTypeStatic,
+    .flag = SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,
+    .decoder = &subghz_protocol_protoview_decoder,
+};
+
+const SubGhzProtocol* protoview_protocol_registry_items[] = {
+    &subghz_protocol_protoview,
+};
+
+const SubGhzProtocolRegistry protoview_protocol_registry = {
+    .items = protoview_protocol_registry_items,
+    .size = COUNT_OF(protoview_protocol_registry_items)
+};