Просмотр исходного кода

Add spectrum_analyzer from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: spectrum_analyzer
git-subtree-mainline: aa5f69c5f58035de3bb56f4aceeb45076f721f3d
git-subtree-split: c76d8299248b1cdd682510293b77e3e6299c8825
Willy-JL 2 лет назад
Родитель
Сommit
4e4bbfce50

+ 1 - 0
spectrum_analyzer/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev base_pack/spectrum_analyzer

+ 7 - 0
spectrum_analyzer/README.md

@@ -0,0 +1,7 @@
+This application allows you to plot a chart showing the relationship between amplitude and frequency, detecting nearby signal sources. If there is a nearby source broadcasting a signal at the observed frequency, the graph will go up sharply.
+
+The app has the following controls:
+
+- The OK button adjusts the width of the spectrum.
+- The Up and Down buttons zoom in and out.
+- The Left and Right buttons switch between different frequency bands.

+ 14 - 0
spectrum_analyzer/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="spectrum_analyzer",
+    name="Spectrum Analyzer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="spectrum_analyzer_app",
+    requires=["gui"],
+    stack_size=2 * 1024,
+    order=12,
+    fap_icon="spectrum_10px.png",
+    fap_category="Sub-GHz",
+    fap_author="@xMasterX & @theY4Kman & @ALEEF02 (original by @jolcese)",
+    fap_version="1.2",
+    fap_description="Displays a spectrogram chart to visually represent RF signals around you.",
+)

+ 64 - 0
spectrum_analyzer/helpers/radio_device_loader.c

@@ -0,0 +1,64 @@
+#include "radio_device_loader.h"
+
+#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+
+static void radio_device_loader_power_on() {
+    uint8_t attempts = 0;
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        //CC1101 power-up time
+        furi_delay_ms(10);
+    }
+}
+
+static void radio_device_loader_power_off() {
+    if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+}
+
+bool radio_device_loader_is_connect_external(const char* name) {
+    bool is_connect = false;
+    bool is_otg_enabled = furi_hal_power_is_otg_enabled();
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_on();
+    }
+
+    const SubGhzDevice* device = subghz_devices_get_by_name(name);
+    if(device) {
+        is_connect = subghz_devices_is_connect(device);
+    }
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_off();
+    }
+    return is_connect;
+}
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type) {
+    const SubGhzDevice* radio_device;
+
+    if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
+       radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
+        radio_device_loader_power_on();
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
+        subghz_devices_begin(radio_device);
+    } else if(current_radio_device == NULL) {
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    } else {
+        radio_device_loader_end(current_radio_device);
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    }
+
+    return radio_device;
+}
+
+void radio_device_loader_end(const SubGhzDevice* radio_device) {
+    furi_assert(radio_device);
+    radio_device_loader_power_off();
+    if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
+        subghz_devices_end(radio_device);
+    }
+}

+ 15 - 0
spectrum_analyzer/helpers/radio_device_loader.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <lib/subghz/devices/devices.h>
+
+/** SubGhzRadioDeviceType */
+typedef enum {
+    SubGhzRadioDeviceTypeInternal,
+    SubGhzRadioDeviceTypeExternalCC1101,
+} SubGhzRadioDeviceType;
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type);
+
+void radio_device_loader_end(const SubGhzDevice* radio_device);

BIN
spectrum_analyzer/img/1.png


BIN
spectrum_analyzer/img/2.png


BIN
spectrum_analyzer/img/3.png


BIN
spectrum_analyzer/spectrum_10px.png


+ 611 - 0
spectrum_analyzer/spectrum_analyzer.c

@@ -0,0 +1,611 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include "spectrum_analyzer.h"
+
+#include <lib/drivers/cc1101_regs.h>
+#include "spectrum_analyzer_worker.h"
+
+typedef struct {
+    uint32_t center_freq;
+    uint8_t width;
+    uint8_t modulation;
+    uint8_t band;
+    uint8_t vscroll;
+
+    uint32_t channel0_frequency;
+    uint32_t spacing;
+
+    bool mode_change;
+    bool modulation_change;
+
+    float max_rssi;
+    uint8_t max_rssi_dec;
+    uint8_t max_rssi_channel;
+    uint8_t channel_ss[NUM_CHANNELS];
+} SpectrumAnalyzerModel;
+
+typedef struct {
+    SpectrumAnalyzerModel* model;
+    FuriMutex* model_mutex;
+
+    FuriMessageQueue* event_queue;
+
+    ViewPort* view_port;
+    Gui* gui;
+
+    SpectrumAnalyzerWorker* worker;
+} SpectrumAnalyzer;
+
+void spectrum_analyzer_draw_scale(Canvas* canvas, const SpectrumAnalyzerModel* model) {
+    // Draw line
+    canvas_draw_line(
+        canvas, FREQ_START_X, FREQ_BOTTOM_Y, FREQ_START_X + FREQ_LENGTH_X, FREQ_BOTTOM_Y);
+    // Draw minor scale
+    for(int i = FREQ_START_X; i < FREQ_START_X + FREQ_LENGTH_X; i += 5) {
+        canvas_draw_line(canvas, i, FREQ_BOTTOM_Y, i, FREQ_BOTTOM_Y + 2);
+    }
+    // Draw major scale
+    for(int i = FREQ_START_X; i < FREQ_START_X + FREQ_LENGTH_X; i += 25) {
+        canvas_draw_line(canvas, i, FREQ_BOTTOM_Y, i, FREQ_BOTTOM_Y + 4);
+    }
+
+    // Draw scale tags
+    uint32_t tag_left = 0;
+    uint32_t tag_center = 0;
+    uint32_t tag_right = 0;
+    char temp_str[18];
+
+    tag_center = model->center_freq;
+
+    switch(model->width) {
+    case NARROW:
+        tag_left = model->center_freq - 2000;
+        tag_right = model->center_freq + 2000;
+        break;
+    case ULTRANARROW:
+        tag_left = model->center_freq - 1000;
+        tag_right = model->center_freq + 1000;
+        break;
+    case PRECISE:
+        tag_left = model->center_freq - 200;
+        tag_right = model->center_freq + 200;
+        break;
+    case ULTRAWIDE:
+        tag_left = model->center_freq - 40000;
+        tag_right = model->center_freq + 40000;
+        break;
+    default:
+        tag_left = model->center_freq - 10000;
+        tag_right = model->center_freq + 10000;
+    }
+
+    canvas_set_font(canvas, FontSecondary);
+    switch(model->width) {
+    case PRECISE:
+    case ULTRANARROW:
+        snprintf(temp_str, 18, "%.1f", ((double)tag_left) / 1000);
+        canvas_draw_str_aligned(canvas, FREQ_START_X, 63, AlignCenter, AlignBottom, temp_str);
+        snprintf(temp_str, 18, "%.1f", ((double)tag_center) / 1000);
+        canvas_draw_str_aligned(canvas, 128 / 2, 63, AlignCenter, AlignBottom, temp_str);
+        snprintf(temp_str, 18, "%.1f", ((double)tag_right) / 1000);
+        canvas_draw_str_aligned(
+            canvas, FREQ_START_X + FREQ_LENGTH_X - 1, 63, AlignCenter, AlignBottom, temp_str);
+        break;
+    default:
+        snprintf(temp_str, 18, "%lu", tag_left / 1000);
+        canvas_draw_str_aligned(canvas, FREQ_START_X, 63, AlignCenter, AlignBottom, temp_str);
+        snprintf(temp_str, 18, "%lu", tag_center / 1000);
+        canvas_draw_str_aligned(canvas, 128 / 2, 63, AlignCenter, AlignBottom, temp_str);
+        snprintf(temp_str, 18, "%lu", tag_right / 1000);
+        canvas_draw_str_aligned(
+            canvas, FREQ_START_X + FREQ_LENGTH_X - 1, 63, AlignCenter, AlignBottom, temp_str);
+    }
+}
+
+static void spectrum_analyzer_render_callback(Canvas* const canvas, void* ctx) {
+    SpectrumAnalyzer* spectrum_analyzer = ctx;
+    //furi_check(furi_mutex_acquire(spectrum_analyzer->model_mutex, FuriWaitForever) == FuriStatusOk);
+
+    SpectrumAnalyzerModel* model = spectrum_analyzer->model;
+
+    spectrum_analyzer_draw_scale(canvas, model);
+
+    for(uint8_t column = 0; column < 128; column++) {
+        uint8_t ss = model->channel_ss[column + 2];
+        // Compress height to max of 64 values (255>>2)
+        uint8_t s = MAX((ss - model->vscroll) >> 2, 0);
+        uint8_t y = FREQ_BOTTOM_Y - s; // bar height
+
+        // Draw each bar
+        canvas_draw_line(canvas, column, FREQ_BOTTOM_Y, column, y);
+    }
+
+    if(model->mode_change) {
+        char temp_mode_str[12];
+        switch(model->width) {
+        case NARROW:
+            strncpy(temp_mode_str, "NARROW", 12);
+            break;
+        case ULTRANARROW:
+            strncpy(temp_mode_str, "ULTRANARROW", 12);
+            break;
+        case PRECISE:
+            strncpy(temp_mode_str, "PRECISE", 12);
+            break;
+        case ULTRAWIDE:
+            strncpy(temp_mode_str, "ULTRAWIDE", 12);
+            break;
+        default:
+            strncpy(temp_mode_str, "WIDE", 12);
+            break;
+        }
+
+        // Current mode label
+        char tmp_str[21];
+        snprintf(tmp_str, 21, "Mode: %s", temp_mode_str);
+        canvas_draw_str_aligned(canvas, 127, 4, AlignRight, AlignTop, tmp_str);
+    }
+
+    if(model->modulation_change) {
+        char temp_mod_str[12];
+        switch(model->modulation) {
+        case NARROW_MODULATION:
+            strncpy(temp_mod_str, "NARROW", 12);
+            break;
+        default:
+            strncpy(temp_mod_str, "DEFAULT", 12);
+            break;
+        }
+
+        // Current modulation label
+        char tmp_str[27];
+        snprintf(tmp_str, 27, "Modulation: %s", temp_mod_str);
+        canvas_draw_str_aligned(canvas, 127, 4, AlignRight, AlignTop, tmp_str);
+    }
+
+    // Draw cross and label
+    if(model->max_rssi > PEAK_THRESHOLD) {
+        // Compress height to max of 64 values (255>>2)
+        uint8_t max_y = MAX((model->max_rssi_dec - model->vscroll) >> 2, 0);
+        max_y = (FREQ_BOTTOM_Y - max_y);
+
+        // Cross
+        int16_t x1, x2, y1, y2;
+        x1 = model->max_rssi_channel - 2 - 2;
+        if(x1 < 0) x1 = 0;
+        y1 = max_y - 2;
+        if(y1 < 0) y1 = 0;
+        x2 = model->max_rssi_channel - 2 + 2;
+        if(x2 > 127) x2 = 127;
+        y2 = max_y + 2;
+        if(y2 > 63) y2 = 63; // SHOULD NOT HAPPEN CHECK!
+        canvas_draw_line(canvas, x1, y1, x2, y2);
+
+        x1 = model->max_rssi_channel - 2 + 2;
+        if(x1 > 127) x1 = 127;
+        y1 = max_y - 2;
+        if(y1 < 0) y1 = 0;
+        x2 = model->max_rssi_channel - 2 - 2;
+        if(x2 < 0) x2 = 0;
+        y2 = max_y + 2;
+        if(y2 > 63) y2 = 63; // SHOULD NOT HAPPEN CHECK!
+        canvas_draw_line(canvas, (uint8_t)x1, (uint8_t)y1, (uint8_t)x2, (uint8_t)y2);
+
+        // Label
+        char temp_str[36];
+        snprintf(
+            temp_str,
+            36,
+            "Peak: %3.2f Mhz %3.1f dbm",
+            ((double)(model->channel0_frequency + (model->max_rssi_channel * model->spacing)) /
+             1000000),
+            (double)model->max_rssi);
+        canvas_draw_str_aligned(canvas, 127, 0, AlignRight, AlignTop, temp_str);
+    }
+
+    //furi_mutex_release(spectrum_analyzer->model_mutex);
+
+    // FURI_LOG_D("Spectrum", "model->vscroll %u", model->vscroll);
+}
+
+static void spectrum_analyzer_input_callback(InputEvent* input_event, void* ctx) {
+    SpectrumAnalyzer* spectrum_analyzer = ctx;
+    // Handle short and long presses
+    if(input_event->type == InputTypeShort || input_event->type == InputTypeLong) {
+        furi_message_queue_put(spectrum_analyzer->event_queue, input_event, FuriWaitForever);
+    }
+}
+
+static void spectrum_analyzer_worker_callback(
+    void* channel_ss,
+    float max_rssi,
+    uint8_t max_rssi_dec,
+    uint8_t max_rssi_channel,
+    void* context) {
+    SpectrumAnalyzer* spectrum_analyzer = context;
+    furi_check(
+        furi_mutex_acquire(spectrum_analyzer->model_mutex, FuriWaitForever) == FuriStatusOk);
+
+    SpectrumAnalyzerModel* model = (SpectrumAnalyzerModel*)spectrum_analyzer->model;
+    memcpy(model->channel_ss, (uint8_t*)channel_ss, sizeof(uint8_t) * NUM_CHANNELS);
+    model->max_rssi = max_rssi;
+    model->max_rssi_dec = max_rssi_dec;
+    model->max_rssi_channel = max_rssi_channel;
+
+    furi_mutex_release(spectrum_analyzer->model_mutex);
+    view_port_update(spectrum_analyzer->view_port);
+}
+
+void spectrum_analyzer_calculate_frequencies(SpectrumAnalyzerModel* model) {
+    // REDO ALL THIS. CALCULATE ONLY WITH SPACING!
+
+    uint8_t new_band;
+    uint32_t min_hz;
+    uint32_t max_hz;
+    uint32_t margin;
+    uint32_t step;
+    uint32_t upper_limit;
+    uint32_t lower_limit;
+    uint32_t next_up;
+    uint32_t next_down;
+    uint8_t next_band_up;
+    uint8_t next_band_down;
+
+    switch(model->width) {
+    case NARROW:
+        margin = NARROW_MARGIN;
+        step = NARROW_STEP;
+        model->spacing = NARROW_SPACING;
+        break;
+    case ULTRANARROW:
+        margin = ULTRANARROW_MARGIN;
+        step = ULTRANARROW_STEP;
+        model->spacing = ULTRANARROW_SPACING;
+        break;
+    case PRECISE:
+        margin = PRECISE_MARGIN;
+        step = PRECISE_STEP;
+        model->spacing = PRECISE_SPACING;
+        break;
+    case ULTRAWIDE:
+        margin = ULTRAWIDE_MARGIN;
+        step = ULTRAWIDE_STEP;
+        model->spacing = ULTRAWIDE_SPACING;
+        /* nearest 20 MHz step */
+        model->center_freq = ((model->center_freq + 10000) / 20000) * 20000;
+        break;
+    default:
+        margin = WIDE_MARGIN;
+        step = WIDE_STEP;
+        model->spacing = WIDE_SPACING;
+        /* nearest 5 MHz step */
+        model->center_freq = ((model->center_freq + 2000) / 5000) * 5000;
+        break;
+    }
+
+    /* handle cases near edges of bands */
+    if(model->center_freq > EDGE_900) {
+        new_band = BAND_900;
+        upper_limit = UPPER(MAX_900, margin, step);
+        lower_limit = LOWER(MIN_900, margin, step);
+        next_up = LOWER(MIN_300, margin, step);
+        next_down = UPPER(MAX_400, margin, step);
+        next_band_up = BAND_300;
+        next_band_down = BAND_400;
+    } else if(model->center_freq > EDGE_400) {
+        new_band = BAND_400;
+        upper_limit = UPPER(MAX_400, margin, step);
+        lower_limit = LOWER(MIN_400, margin, step);
+        next_up = LOWER(MIN_900, margin, step);
+        next_down = UPPER(MAX_300, margin, step);
+        next_band_up = BAND_900;
+        next_band_down = BAND_300;
+    } else {
+        new_band = BAND_300;
+        upper_limit = UPPER(MAX_300, margin, step);
+        lower_limit = LOWER(MIN_300, margin, step);
+        next_up = LOWER(MIN_400, margin, step);
+        next_down = UPPER(MAX_900, margin, step);
+        next_band_up = BAND_400;
+        next_band_down = BAND_900;
+    }
+
+    if(model->center_freq > upper_limit) {
+        model->center_freq = upper_limit;
+        if(new_band == model->band) {
+            new_band = next_band_up;
+            model->center_freq = next_up;
+        }
+    } else if(model->center_freq < lower_limit) {
+        model->center_freq = lower_limit;
+        if(new_band == model->band) {
+            new_band = next_band_down;
+            model->center_freq = next_down;
+        }
+    }
+
+    model->band = new_band;
+    /* doing everything in Hz from here on */
+    switch(model->band) {
+    case BAND_400:
+        min_hz = MIN_400 * 1000;
+        max_hz = MAX_400 * 1000;
+        break;
+    case BAND_300:
+        min_hz = MIN_300 * 1000;
+        max_hz = MAX_300 * 1000;
+        break;
+    default:
+        min_hz = MIN_900 * 1000;
+        max_hz = MAX_900 * 1000;
+        break;
+    }
+
+    model->channel0_frequency =
+        model->center_freq * 1000 - (model->spacing * ((NUM_CHANNELS / 2) + 1));
+
+    // /* calibrate upper channels */
+    // hz = model->center_freq * 1000000;
+    // max_chan = NUM_CHANNELS / 2;
+    // while (hz <= max_hz && max_chan < NUM_CHANNELS) {
+    //     instance->chan_table[max_chan].frequency = hz;
+    //     FURI_LOG_T("Spectrum", "calibrate_freq ch[%u]: %lu", max_chan, hz);
+    //     hz += model->spacing;
+    //     max_chan++;
+    // }
+
+    // /* calibrate lower channels */
+    // hz = instance->freq * 1000000 - model->spacing;
+    // min_chan = NUM_CHANNELS / 2;
+    // while (hz >= min_hz && min_chan > 0) {
+    //     min_chan--;
+    //     instance->chan_table[min_chan].frequency = hz;
+    //     FURI_LOG_T("Spectrum", "calibrate_freq ch[%u]: %lu", min_chan, hz);
+    //     hz -= model->spacing;
+    // }
+
+    model->max_rssi = -200.0;
+    model->max_rssi_dec = 0;
+
+    FURI_LOG_D("Spectrum", "setup_frequencies - max_hz: %lu - min_hz: %lu", max_hz, min_hz);
+    FURI_LOG_D("Spectrum", "center_freq: %lu", model->center_freq);
+    FURI_LOG_D(
+        "Spectrum",
+        "ch[0]: %lu - ch[%u]: %lu",
+        model->channel0_frequency,
+        NUM_CHANNELS - 1,
+        model->channel0_frequency + ((NUM_CHANNELS - 1) * model->spacing));
+}
+
+SpectrumAnalyzer* spectrum_analyzer_alloc() {
+    SpectrumAnalyzer* instance = malloc(sizeof(SpectrumAnalyzer));
+    instance->model = malloc(sizeof(SpectrumAnalyzerModel));
+
+    SpectrumAnalyzerModel* model = instance->model;
+
+    for(uint8_t ch = 0; ch < NUM_CHANNELS - 1; ch++) {
+        model->channel_ss[ch] = 0;
+    }
+    model->max_rssi_dec = 0;
+    model->max_rssi_channel = 0;
+    model->max_rssi = PEAK_THRESHOLD - 1; // Should initializar to < PEAK_THRESHOLD
+
+    model->center_freq = DEFAULT_FREQ;
+    model->width = WIDE;
+    model->modulation = DEFAULT_MODULATION;
+    model->band = BAND_400;
+
+    model->vscroll = DEFAULT_VSCROLL;
+
+    instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    instance->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    instance->worker = spectrum_analyzer_worker_alloc();
+
+    spectrum_analyzer_worker_set_callback(
+        instance->worker, spectrum_analyzer_worker_callback, instance);
+
+    // Set system callbacks
+    instance->view_port = view_port_alloc();
+    view_port_draw_callback_set(instance->view_port, spectrum_analyzer_render_callback, instance);
+    view_port_input_callback_set(instance->view_port, spectrum_analyzer_input_callback, instance);
+
+    // Open GUI and register view_port
+    instance->gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen);
+
+    return instance;
+}
+
+void spectrum_analyzer_free(SpectrumAnalyzer* instance) {
+    // view_port_enabled_set(view_port, false);
+    gui_remove_view_port(instance->gui, instance->view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(instance->view_port);
+
+    spectrum_analyzer_worker_free(instance->worker);
+
+    furi_message_queue_free(instance->event_queue);
+
+    furi_mutex_free(instance->model_mutex);
+
+    free(instance->model);
+    free(instance);
+}
+
+int32_t spectrum_analyzer_app(void* p) {
+    UNUSED(p);
+
+    SpectrumAnalyzer* spectrum_analyzer = spectrum_analyzer_alloc();
+    InputEvent input;
+
+    furi_hal_power_suppress_charge_enter();
+
+    FURI_LOG_D("Spectrum", "Main Loop - Starting worker");
+    furi_delay_ms(50);
+
+    spectrum_analyzer_worker_start(spectrum_analyzer->worker);
+    spectrum_analyzer_calculate_frequencies(spectrum_analyzer->model);
+    spectrum_analyzer_worker_set_frequencies(
+        spectrum_analyzer->worker,
+        spectrum_analyzer->model->channel0_frequency,
+        spectrum_analyzer->model->spacing,
+        spectrum_analyzer->model->width);
+
+    FURI_LOG_D("Spectrum", "Main Loop - Wait on queue");
+    furi_delay_ms(50);
+
+    while(furi_message_queue_get(spectrum_analyzer->event_queue, &input, FuriWaitForever) ==
+          FuriStatusOk) {
+        furi_check(
+            furi_mutex_acquire(spectrum_analyzer->model_mutex, FuriWaitForever) == FuriStatusOk);
+
+        FURI_LOG_D("Spectrum", "Main Loop - Input: %u", input.key);
+
+        SpectrumAnalyzerModel* model = spectrum_analyzer->model;
+
+        uint8_t vstep = VERTICAL_SHORT_STEP;
+        uint32_t hstep;
+
+        bool exit_loop = false;
+
+        switch(model->width) {
+        case NARROW:
+            hstep = NARROW_STEP;
+            break;
+        case ULTRANARROW:
+            hstep = ULTRANARROW_STEP;
+            break;
+        case ULTRAWIDE:
+            hstep = ULTRAWIDE_STEP;
+            break;
+        case PRECISE:
+            hstep = PRECISE_STEP;
+            break;
+        default:
+            hstep = WIDE_STEP;
+            break;
+        }
+
+        switch(input.type) {
+        case InputTypeShort:
+            switch(input.key) {
+            case InputKeyUp:
+                model->vscroll = MAX(model->vscroll - vstep, MIN_VSCROLL);
+                FURI_LOG_D("Spectrum", "Vscroll: %u", model->vscroll);
+                break;
+            case InputKeyDown:
+                model->vscroll = MIN(model->vscroll + vstep, MAX_VSCROLL);
+                FURI_LOG_D("Spectrum", "Vscroll: %u", model->vscroll);
+                break;
+            case InputKeyRight:
+                model->center_freq += hstep;
+                FURI_LOG_D("Spectrum", "center_freq: %lu", model->center_freq);
+                spectrum_analyzer_calculate_frequencies(model);
+                spectrum_analyzer_worker_set_frequencies(
+                    spectrum_analyzer->worker,
+                    model->channel0_frequency,
+                    model->spacing,
+                    model->width);
+                break;
+            case InputKeyLeft:
+                model->center_freq -= hstep;
+                spectrum_analyzer_calculate_frequencies(model);
+                spectrum_analyzer_worker_set_frequencies(
+                    spectrum_analyzer->worker,
+                    model->channel0_frequency,
+                    model->spacing,
+                    model->width);
+                FURI_LOG_D("Spectrum", "center_freq: %lu", model->center_freq);
+                break;
+            case InputKeyOk: {
+                switch(model->width) {
+                case WIDE:
+                    model->width = NARROW;
+                    break;
+                case NARROW:
+                    model->width = ULTRANARROW;
+                    break;
+                case ULTRANARROW:
+                    model->width = PRECISE;
+                    break;
+                case PRECISE:
+                    model->width = ULTRAWIDE;
+                    break;
+                case ULTRAWIDE:
+                    model->width = WIDE;
+                    break;
+                default:
+                    model->width = WIDE;
+                    break;
+                }
+            }
+                model->mode_change = true;
+                view_port_update(spectrum_analyzer->view_port);
+
+                furi_delay_ms(1000);
+
+                model->mode_change = false;
+                spectrum_analyzer_calculate_frequencies(model);
+                spectrum_analyzer_worker_set_frequencies(
+                    spectrum_analyzer->worker,
+                    model->channel0_frequency,
+                    model->spacing,
+                    model->width);
+                FURI_LOG_D("Spectrum", "Width: %u", model->width);
+                break;
+            case InputKeyBack:
+                exit_loop = true;
+                break;
+            default:
+                break;
+            }
+            break;
+        case InputTypeLong:
+            switch(input.key) {
+            case InputKeyOk:
+                FURI_LOG_D("Spectrum", "InputTypeLong");
+                switch(model->modulation) {
+                case NARROW_MODULATION:
+                    model->modulation = DEFAULT_MODULATION;
+                    break;
+                case DEFAULT_MODULATION:
+                default:
+                    model->modulation = NARROW_MODULATION;
+                    break;
+                }
+
+                model->modulation_change = true;
+                view_port_update(spectrum_analyzer->view_port);
+
+                furi_delay_ms(1000);
+
+                model->modulation_change = false;
+                spectrum_analyzer_worker_set_modulation(
+                    spectrum_analyzer->worker, spectrum_analyzer->model->modulation);
+                break;
+            default:
+                break;
+            }
+            break;
+        default:
+            break;
+        }
+
+        furi_mutex_release(spectrum_analyzer->model_mutex);
+        view_port_update(spectrum_analyzer->view_port);
+        if(exit_loop == true) break;
+    }
+
+    spectrum_analyzer_worker_stop(spectrum_analyzer->worker);
+
+    furi_hal_power_suppress_charge_exit();
+
+    spectrum_analyzer_free(spectrum_analyzer);
+
+    return 0;
+}

+ 84 - 0
spectrum_analyzer/spectrum_analyzer.h

@@ -0,0 +1,84 @@
+#define NUM_CHANNELS 132
+#define NUM_CHUNKS 6
+#define CHUNK_SIZE (NUM_CHANNELS / NUM_CHUNKS)
+
+// Screen coordinates
+#define FREQ_BOTTOM_Y 50
+#define FREQ_START_X 14
+// How many channels displayed on the scale (On screen still 218)
+#define FREQ_LENGTH_X 102
+// dBm threshold to show peak value
+#define PEAK_THRESHOLD -85
+
+/*
+ * ultrawide mode: 80 MHz on screen, 784 kHz per channel
+ * wide mode (default): 20 MHz on screen, 196 kHz per channel
+ * narrow mode: 4 MHz on screen, 39 kHz per channel
+ * ultranarrow mode: 2 MHz on screen, 19 kHz per channel
+ * precise mode: 400 KHz on screen, 3.92 kHz per channel
+ */
+#define WIDE 0
+#define NARROW 1
+#define ULTRAWIDE 2
+#define ULTRANARROW 3
+#define PRECISE 4
+
+/* channel spacing in Hz */
+#define WIDE_SPACING 196078
+#define NARROW_SPACING 39215
+#define ULTRAWIDE_SPACING 784313
+#define ULTRANARROW_SPACING 19607
+#define PRECISE_SPACING 3921
+
+/* vertical scrolling */
+#define VERTICAL_SHORT_STEP 16
+#define MAX_VSCROLL 120
+#define MIN_VSCROLL 0
+#define DEFAULT_VSCROLL 48
+
+/* frequencies in KHz */
+#define DEFAULT_FREQ 440000
+#define WIDE_STEP 5000
+#define NARROW_STEP 1000
+#define ULTRAWIDE_STEP 20000
+#define ULTRANARROW_STEP 500
+#define PRECISE_STEP 100
+
+/* margin in KHz */
+#define WIDE_MARGIN 13000
+#define NARROW_MARGIN 3000
+#define ULTRAWIDE_MARGIN 42000
+#define ULTRANARROW_MARGIN 1000
+#define PRECISE_MARGIN 200
+
+/* frequency bands supported by device */
+#define BAND_300 0
+#define BAND_400 1
+#define BAND_900 2
+
+/* band limits in KHz */
+#define MIN_300 281000
+#define CEN_300 315000
+#define MAX_300 361000
+#define MIN_400 378000
+#define CEN_400 435000
+#define MAX_400 481000
+#define MIN_900 749000
+#define CEN_900 855000
+#define MAX_900 962000
+
+/* band transition points in KHz */
+#define EDGE_400 369000
+#define EDGE_900 615000
+
+/* VCO transition points in Hz */
+#define MID_300 318000000
+#define MID_400 424000000
+#define MID_900 848000000
+
+#define UPPER(a, b, c) ((((a) - (b) + ((c) / 2)) / (c)) * (c))
+#define LOWER(a, b, c) ((((a) + (b)) / (c)) * (c))
+
+/* Modulation references */
+#define DEFAULT_MODULATION 0
+#define NARROW_MODULATION 1

+ 345 - 0
spectrum_analyzer/spectrum_analyzer_worker.c

@@ -0,0 +1,345 @@
+#include "spectrum_analyzer.h"
+#include "spectrum_analyzer_worker.h"
+
+#include <furi_hal.h>
+#include <furi.h>
+
+#include "helpers/radio_device_loader.h"
+
+#include <lib/drivers/cc1101_regs.h>
+
+struct SpectrumAnalyzerWorker {
+    FuriThread* thread;
+    bool should_work;
+
+    SpectrumAnalyzerWorkerCallback callback;
+    void* callback_context;
+
+    const SubGhzDevice* radio_device;
+
+    uint32_t channel0_frequency;
+    uint32_t spacing;
+    uint8_t width;
+    uint8_t modulation;
+    float max_rssi;
+    uint8_t max_rssi_dec;
+    uint8_t max_rssi_channel;
+
+    uint8_t channel_ss[NUM_CHANNELS];
+};
+
+/* set the channel bandwidth */
+void spectrum_analyzer_worker_set_filter(SpectrumAnalyzerWorker* instance) {
+    uint8_t filter_config[2][2] = {
+        {CC1101_MDMCFG4, 0},
+        {0, 0},
+    };
+
+    // FURI_LOG_D("SpectrumWorker", "spectrum_analyzer_worker_set_filter: width = %u", instance->width);
+
+    /* channel spacing should fit within 80% of channel filter bandwidth */
+    switch(instance->width) {
+    case NARROW:
+        filter_config[0][1] = 0xFC; /* 39.2 kHz / .8 = 49 kHz --> 58 kHz */
+        break;
+    case ULTRAWIDE:
+        filter_config[0][1] = 0x0C; /* 784 kHz / .8 = 980 kHz --> 812 kHz */
+        break;
+    default:
+        filter_config[0][1] = 0x6C; /* 196 kHz / .8 = 245 kHz --> 270 kHz */
+        break;
+    }
+
+    UNUSED(filter_config);
+    // furi_hal_subghz_load_registers((uint8_t*)filter_config);
+}
+
+static int32_t spectrum_analyzer_worker_thread(void* context) {
+    furi_assert(context);
+    SpectrumAnalyzerWorker* instance = context;
+
+    FURI_LOG_D("SpectrumWorker", "spectrum_analyzer_worker_thread: Start");
+
+    // Start CC1101
+    subghz_devices_reset(instance->radio_device);
+    subghz_devices_load_preset(instance->radio_device, FuriHalSubGhzPresetOok650Async, NULL);
+    subghz_devices_set_frequency(instance->radio_device, 433920000);
+    subghz_devices_flush_rx(instance->radio_device);
+    subghz_devices_set_rx(instance->radio_device);
+
+    // Default modulation
+    const uint8_t default_modulation[] = {
+
+        /* Frequency Synthesizer Control */
+        CC1101_FSCTRL0,
+        0x00,
+        CC1101_FSCTRL1,
+        0x12, // IF = (26*10^6) / (2^10) * 0x12 = 304687.5 Hz
+
+        // Modem Configuration
+        // CC1101_MDMCFG0,
+        // 0x00, // Channel spacing is 25kHz
+        // CC1101_MDMCFG1,
+        // 0x00, // Channel spacing is 25kHz
+        // CC1101_MDMCFG2,
+        // 0x30, // Format ASK/OOK, No preamble/sync
+        // CC1101_MDMCFG3,
+        // 0x32, // Data rate is 121.399 kBaud
+        CC1101_MDMCFG4,
+        0x6C, // Rx BW filter is 270.83 kHz
+
+        /* Frequency Offset Compensation Configuration */
+        // CC1101_FOCCFG,
+        // 0x18, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+        /* Automatic Gain Control */
+        // CC1101_AGCCTRL0,
+        // 0x91, // 10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
+        // CC1101_AGCCTRL1,
+        // 0x0, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
+        CC1101_AGCCTRL2,
+        0xC0, // 03 - The 3 highest DVGA gain settings can not be used; 000 - MAX LNA+LNA2; 000 - MAIN_TARGET 24 dB
+
+        /* Frontend configuration */
+        // CC1101_FREND0,
+        // 0x11, // Adjusts current TX LO buffer + high is PATABLE[1]
+        // CC1101_FREND1,
+        // 0xB6, //
+
+        CC1101_TEST2,
+        0x88,
+        CC1101_TEST1,
+        0x31,
+        CC1101_TEST0,
+        0x09,
+
+        /* End  */
+        0,
+        0,
+
+        // ook_async_patable
+        0x00,
+        0xC0, // 12dBm 0xC0, 10dBm 0xC5, 7dBm 0xCD, 5dBm 0x86, 0dBm 0x50, -6dBm 0x37, -10dBm 0x26, -15dBm 0x1D, -20dBm 0x17, -30dBm 0x03
+        0x00,
+        0x00,
+        0x00,
+        0x00,
+        0x00,
+        0x00};
+
+    // Narrow modulation
+    const uint8_t narrow_modulation[] = {
+
+        /* Frequency Synthesizer Control */
+        CC1101_FSCTRL0,
+        0x00,
+        CC1101_FSCTRL1,
+        0x00, // IF = (26*10^6) / (2^10) * 0x00 = 0 Hz
+
+        // Modem Configuration
+        // CC1101_MDMCFG0,
+        // 0x00, // Channel spacing is 25kHz
+        // CC1101_MDMCFG1,
+        // 0x00, // Channel spacing is 25kHz
+        // CC1101_MDMCFG2,
+        // 0x30, // Format ASK/OOK, No preamble/sync
+        // CC1101_MDMCFG3,
+        // 0x32, // Data rate is 121.399 kBaud
+        CC1101_MDMCFG4,
+        0xFC, // Rx BW filter is 58.04 kHz
+
+        /* Frequency Offset Compensation Configuration */
+        // CC1101_FOCCFG,
+        // 0x18, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+        /* Automatic Gain Control */
+        CC1101_AGCCTRL0,
+        0x30, // 00 - NO hysteresis, symmetric dead zone, high gain ; 11 - 32 samples agc; 00 - Normal AGC, 00 - 8dB boundary
+        CC1101_AGCCTRL1,
+        0x0, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
+        CC1101_AGCCTRL2,
+        0x84, // 02 - The 2 highest DVGA gain settings can not be used; 000 - MAX LNA+LNA2; 100 - MAIN_TARGET 36 dB
+
+        /* Frontend configuration */
+        // CC1101_FREND0,
+        // 0x11, // Adjusts current TX LO buffer + high is PATABLE[1]
+        // CC1101_FREND1,
+        // 0xB6, //
+
+        CC1101_TEST2,
+        0x88,
+        CC1101_TEST1,
+        0x31,
+        CC1101_TEST0,
+        0x09,
+
+        /* End  */
+        0,
+        0,
+
+        // ook_async_patable
+        0x00,
+        0xC0, // 12dBm 0xC0, 10dBm 0xC5, 7dBm 0xCD, 5dBm 0x86, 0dBm 0x50, -6dBm 0x37, -10dBm 0x26, -15dBm 0x1D, -20dBm 0x17, -30dBm 0x03
+        0x00,
+        0x00,
+        0x00,
+        0x00,
+        0x00,
+        0x00};
+
+    const uint8_t* modulations[] = {default_modulation, narrow_modulation};
+
+    while(instance->should_work) {
+        furi_delay_ms(50);
+
+        // FURI_LOG_T("SpectrumWorker", "spectrum_analyzer_worker_thread: Worker Loop");
+        subghz_devices_idle(instance->radio_device);
+        subghz_devices_load_preset(
+            instance->radio_device,
+            FuriHalSubGhzPresetCustom,
+            (uint8_t*)modulations[instance->modulation]);
+        //subghz_devices_load_preset(
+        //    instance->radio_device, FuriHalSubGhzPresetCustom, (uint8_t*)default_modulation);
+        //furi_hal_subghz_load_custom_preset(modulations[instance->modulation]);
+
+        // TODO: Check filter!
+        // spectrum_analyzer_worker_set_filter(instance);
+
+        instance->max_rssi_dec = 0;
+
+        // Visit each channel non-consecutively
+        for(uint8_t ch_offset = 0, chunk = 0; ch_offset < CHUNK_SIZE;
+            ++chunk >= NUM_CHUNKS && ++ch_offset && (chunk = 0)) {
+            uint8_t ch = chunk * CHUNK_SIZE + ch_offset;
+
+            if(subghz_devices_is_frequency_valid(
+                   instance->radio_device,
+                   instance->channel0_frequency + (ch * instance->spacing)))
+                subghz_devices_set_frequency(
+                    instance->radio_device,
+                    instance->channel0_frequency + (ch * instance->spacing));
+
+            subghz_devices_set_rx(instance->radio_device);
+            furi_delay_ms(3);
+
+            //         dec      dBm
+            //max_ss = 127 ->  -10.5
+            //max_ss = 0   ->  -74.0
+            //max_ss = 255 ->  -74.5
+            //max_ss = 128 -> -138.0
+            instance->channel_ss[ch] = (subghz_devices_get_rssi(instance->radio_device) + 138) * 2;
+
+            if(instance->channel_ss[ch] > instance->max_rssi_dec) {
+                instance->max_rssi_dec = instance->channel_ss[ch];
+                instance->max_rssi = (instance->channel_ss[ch] / 2) - 138;
+                instance->max_rssi_channel = ch;
+            }
+
+            subghz_devices_idle(instance->radio_device);
+        }
+
+        // FURI_LOG_T("SpectrumWorker", "channel_ss[0]: %u", instance->channel_ss[0]);
+
+        // Report results back to main thread
+        if(instance->callback) {
+            instance->callback(
+                (void*)&(instance->channel_ss),
+                instance->max_rssi,
+                instance->max_rssi_dec,
+                instance->max_rssi_channel,
+                instance->callback_context);
+        }
+    }
+
+    return 0;
+}
+
+SpectrumAnalyzerWorker* spectrum_analyzer_worker_alloc() {
+    FURI_LOG_D("Spectrum", "spectrum_analyzer_worker_alloc: Start");
+
+    SpectrumAnalyzerWorker* instance = malloc(sizeof(SpectrumAnalyzerWorker));
+
+    instance->thread = furi_thread_alloc();
+    furi_thread_set_name(instance->thread, "SpectrumWorker");
+    furi_thread_set_stack_size(instance->thread, 2048);
+    furi_thread_set_context(instance->thread, instance);
+    furi_thread_set_callback(instance->thread, spectrum_analyzer_worker_thread);
+
+    subghz_devices_init();
+
+    instance->radio_device =
+        radio_device_loader_set(instance->radio_device, SubGhzRadioDeviceTypeExternalCC1101);
+
+    FURI_LOG_D("Spectrum", "spectrum_analyzer_worker_alloc: End");
+
+    return instance;
+}
+
+void spectrum_analyzer_worker_free(SpectrumAnalyzerWorker* instance) {
+    FURI_LOG_D("Spectrum", "spectrum_analyzer_worker_free");
+    furi_assert(instance);
+    furi_thread_free(instance->thread);
+
+    subghz_devices_sleep(instance->radio_device);
+    radio_device_loader_end(instance->radio_device);
+
+    subghz_devices_deinit();
+
+    free(instance);
+}
+
+void spectrum_analyzer_worker_set_callback(
+    SpectrumAnalyzerWorker* instance,
+    SpectrumAnalyzerWorkerCallback callback,
+    void* context) {
+    furi_assert(instance);
+    instance->callback = callback;
+    instance->callback_context = context;
+}
+
+void spectrum_analyzer_worker_set_frequencies(
+    SpectrumAnalyzerWorker* instance,
+    uint32_t channel0_frequency,
+    uint32_t spacing,
+    uint8_t width) {
+    furi_assert(instance);
+
+    FURI_LOG_D(
+        "SpectrumWorker",
+        "spectrum_analyzer_worker_set_frequencies - channel0_frequency= %lu - spacing = %lu - width = %u",
+        channel0_frequency,
+        spacing,
+        width);
+
+    instance->channel0_frequency = channel0_frequency;
+    instance->spacing = spacing;
+    instance->width = width;
+}
+
+void spectrum_analyzer_worker_set_modulation(SpectrumAnalyzerWorker* instance, uint8_t modulation) {
+    furi_assert(instance);
+
+    FURI_LOG_D(
+        "SpectrumWorker", "spectrum_analyzer_worker_set_modulation - modulation = %u", modulation);
+
+    instance->modulation = modulation;
+}
+
+void spectrum_analyzer_worker_start(SpectrumAnalyzerWorker* instance) {
+    FURI_LOG_D("Spectrum", "spectrum_analyzer_worker_start");
+
+    furi_assert(instance);
+    furi_assert(instance->should_work == false);
+
+    instance->should_work = true;
+    furi_thread_start(instance->thread);
+}
+
+void spectrum_analyzer_worker_stop(SpectrumAnalyzerWorker* instance) {
+    FURI_LOG_D("Spectrum", "spectrum_analyzer_worker_stop");
+    furi_assert(instance);
+    furi_assert(instance->should_work == true);
+
+    instance->should_work = false;
+    furi_thread_join(instance->thread);
+}

+ 35 - 0
spectrum_analyzer/spectrum_analyzer_worker.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#include <stdint.h>
+
+typedef void (*SpectrumAnalyzerWorkerCallback)(
+    void* chan_table,
+    float max_rssi,
+    uint8_t max_rssi_dec,
+    uint8_t max_rssi_channel,
+    void* context);
+
+typedef struct SpectrumAnalyzerWorker SpectrumAnalyzerWorker;
+
+SpectrumAnalyzerWorker* spectrum_analyzer_worker_alloc();
+
+void spectrum_analyzer_worker_free(SpectrumAnalyzerWorker* instance);
+
+void spectrum_analyzer_worker_set_callback(
+    SpectrumAnalyzerWorker* instance,
+    SpectrumAnalyzerWorkerCallback callback,
+    void* context);
+
+void spectrum_analyzer_worker_set_filter(SpectrumAnalyzerWorker* instance);
+
+void spectrum_analyzer_worker_set_frequencies(
+    SpectrumAnalyzerWorker* instance,
+    uint32_t channel0_frequency,
+    uint32_t spacing,
+    uint8_t width);
+
+void spectrum_analyzer_worker_set_modulation(SpectrumAnalyzerWorker* instance, uint8_t modulation);
+
+void spectrum_analyzer_worker_start(SpectrumAnalyzerWorker* instance);
+
+void spectrum_analyzer_worker_stop(SpectrumAnalyzerWorker* instance);