|
@@ -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;
|
|
|
|
|
+}
|