From 2572acb8da8fc5406da38723d36aac11614fbfb7 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Thu, 23 Jul 2026 18:49:52 +0300 Subject: [PATCH 1/4] Spectrum Analyzer: hold last peak and persist settings between launches - Keep the last detected peak label on screen (prefixed "Last:" once the signal is gone) until a new peak replaces it, so short bursts can be read after the fact (#100) - Save frequency, spectrum width, modulation and vscroll on exit and restore them on the next launch, with range validation on load (#99) - Apply restored modulation to the worker on startup - Bump fap_version to 1.5 Co-Authored-By: Claude Opus 4.8 (1M context) --- base_pack/spectrum_analyzer/README.md | 6 +- base_pack/spectrum_analyzer/application.fam | 2 +- .../spectrum_analyzer/spectrum_analyzer.c | 86 +++++++++++++++++-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/base_pack/spectrum_analyzer/README.md b/base_pack/spectrum_analyzer/README.md index c1cf6d5d..488df0b9 100644 --- a/base_pack/spectrum_analyzer/README.md +++ b/base_pack/spectrum_analyzer/README.md @@ -4,4 +4,8 @@ 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. \ No newline at end of file +- The Left and Right buttons switch between different frequency bands. + +The last detected peak stays on screen (prefixed "Last:") until a new peak is found, so short bursts are easy to read even after the signal is gone. + +The app saves the current view (frequency, spectrum width, modulation and zoom) on exit and restores it on the next launch. diff --git a/base_pack/spectrum_analyzer/application.fam b/base_pack/spectrum_analyzer/application.fam index 1227c5b6..eaffe429 100644 --- a/base_pack/spectrum_analyzer/application.fam +++ b/base_pack/spectrum_analyzer/application.fam @@ -9,6 +9,6 @@ App( fap_icon="spectrum_10px.png", fap_category="Sub-GHz", fap_author="@xMasterX & @theY4Kman & @ALEEF02 (original by @jolcese)", - fap_version="1.4", + fap_version="1.5", fap_description="Displays a spectrogram chart to visually represent RF signals around you.", ) diff --git a/base_pack/spectrum_analyzer/spectrum_analyzer.c b/base_pack/spectrum_analyzer/spectrum_analyzer.c index ed6e8686..4c1d5b6f 100644 --- a/base_pack/spectrum_analyzer/spectrum_analyzer.c +++ b/base_pack/spectrum_analyzer/spectrum_analyzer.c @@ -4,11 +4,24 @@ #include #include #include +#include +#include #include "spectrum_analyzer.h" #include #include "spectrum_analyzer_worker.h" +#define SPECTRUM_ANALYZER_SETTINGS_PATH APP_DATA_PATH("spectrum_analyzer.save") +#define SPECTRUM_ANALYZER_SETTINGS_MAGIC (0x5A) +#define SPECTRUM_ANALYZER_SETTINGS_VERSION (1) + +typedef struct { + uint32_t center_freq; + uint8_t width; + uint8_t modulation; + uint8_t vscroll; +} SpectrumAnalyzerSettings; + typedef struct { uint32_t center_freq; uint8_t width; @@ -25,6 +38,10 @@ typedef struct { float max_rssi; uint8_t max_rssi_dec; uint8_t max_rssi_channel; + + float last_peak_rssi; + uint32_t last_peak_frequency; + uint8_t channel_ss[NUM_CHANNELS]; } SpectrumAnalyzerModel; @@ -194,16 +211,19 @@ static void spectrum_analyzer_render_callback(Canvas* const canvas, void* ctx) { 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 + // Label: keep showing the last locked peak even after the signal is gone + if((model->last_peak_rssi > PEAK_THRESHOLD) && !model->mode_change && + !model->modulation_change) { 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); + "%s: %3.2f Mhz %3.1f dbm", + (model->max_rssi > PEAK_THRESHOLD) ? "Peak" : "Last", + ((double)model->last_peak_frequency / 1000000), + (double)model->last_peak_rssi); canvas_draw_str_aligned(canvas, 127, 0, AlignRight, AlignTop, temp_str); } @@ -236,6 +256,14 @@ static void spectrum_analyzer_worker_callback( model->max_rssi_dec = max_rssi_dec; model->max_rssi_channel = max_rssi_channel; + // Remember the last peak, frequency is stored in Hz so it stays + // valid after the view is retuned + if(max_rssi > PEAK_THRESHOLD) { + model->last_peak_rssi = max_rssi; + model->last_peak_frequency = + model->channel0_frequency + (max_rssi_channel * model->spacing); + } + furi_mutex_release(spectrum_analyzer->model_mutex); view_port_update(spectrum_analyzer->view_port); } @@ -381,6 +409,46 @@ void spectrum_analyzer_calculate_frequencies(SpectrumAnalyzerModel* model) { model->channel0_frequency + ((NUM_CHANNELS - 1) * model->spacing)); } +static void spectrum_analyzer_load_settings(SpectrumAnalyzerModel* model) { + SpectrumAnalyzerSettings settings; + if(!saved_struct_load( + SPECTRUM_ANALYZER_SETTINGS_PATH, + &settings, + sizeof(settings), + SPECTRUM_ANALYZER_SETTINGS_MAGIC, + SPECTRUM_ANALYZER_SETTINGS_VERSION)) { + return; + } + + // Ignore saved values that are out of range, keep defaults instead + if((settings.center_freq < MIN_300) || (settings.center_freq > MAX_900) || + (settings.width > PRECISE) || (settings.modulation > NARROW_MODULATION) || + (settings.vscroll > MAX_VSCROLL)) { + return; + } + + model->center_freq = settings.center_freq; + model->width = settings.width; + model->modulation = settings.modulation; + model->vscroll = settings.vscroll; +} + +static void spectrum_analyzer_save_settings(const SpectrumAnalyzerModel* model) { + SpectrumAnalyzerSettings settings = { + .center_freq = model->center_freq, + .width = model->width, + .modulation = model->modulation, + .vscroll = model->vscroll, + }; + + saved_struct_save( + SPECTRUM_ANALYZER_SETTINGS_PATH, + &settings, + sizeof(settings), + SPECTRUM_ANALYZER_SETTINGS_MAGIC, + SPECTRUM_ANALYZER_SETTINGS_VERSION); +} + SpectrumAnalyzer* spectrum_analyzer_alloc() { SpectrumAnalyzer* instance = malloc(sizeof(SpectrumAnalyzer)); instance->model = malloc(sizeof(SpectrumAnalyzerModel)); @@ -393,6 +461,8 @@ SpectrumAnalyzer* spectrum_analyzer_alloc() { model->max_rssi_dec = 0; model->max_rssi_channel = 0; model->max_rssi = PEAK_THRESHOLD - 1; // Should initializar to < PEAK_THRESHOLD + model->last_peak_rssi = PEAK_THRESHOLD - 1; + model->last_peak_frequency = 0; model->center_freq = DEFAULT_FREQ; model->width = WIDE; @@ -401,6 +471,8 @@ SpectrumAnalyzer* spectrum_analyzer_alloc() { model->vscroll = DEFAULT_VSCROLL; + spectrum_analyzer_load_settings(model); + instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal); instance->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); @@ -455,6 +527,8 @@ int32_t spectrum_analyzer_app(void* p) { spectrum_analyzer->model->channel0_frequency, spectrum_analyzer->model->spacing, spectrum_analyzer->model->width); + spectrum_analyzer_worker_set_modulation( + spectrum_analyzer->worker, spectrum_analyzer->model->modulation); FURI_LOG_D("Spectrum", "Main Loop - Wait on queue"); furi_delay_ms(50); @@ -603,6 +677,8 @@ int32_t spectrum_analyzer_app(void* p) { spectrum_analyzer_worker_stop(spectrum_analyzer->worker); + spectrum_analyzer_save_settings(spectrum_analyzer->model); + furi_hal_power_suppress_charge_exit(); spectrum_analyzer_free(spectrum_analyzer); From dab17bc32b213a66326768c42815537100dc5a18 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Thu, 23 Jul 2026 19:12:02 +0300 Subject: [PATCH 2/4] Spectrum Analyzer: address review findings - Latch the peak frequency measured by the worker at sweep time and pass it through the callback, so a retune during a sweep cannot latch a mislabeled "Last:" frequency - Log a warning when out-of-range saved settings are discarded - Document the long-press OK modulation toggle in the README and clarify when the "Last:" prefix appears - Fix stale/imprecise comments from the previous commit Co-Authored-By: Claude Opus 4.8 (1M context) --- base_pack/spectrum_analyzer/README.md | 3 ++- .../spectrum_analyzer/spectrum_analyzer.c | 19 +++++++++++-------- .../spectrum_analyzer_worker.c | 14 ++++++++------ .../spectrum_analyzer_worker.h | 1 + 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/base_pack/spectrum_analyzer/README.md b/base_pack/spectrum_analyzer/README.md index 488df0b9..2d459f2d 100644 --- a/base_pack/spectrum_analyzer/README.md +++ b/base_pack/spectrum_analyzer/README.md @@ -3,9 +3,10 @@ This application allows you to plot a chart showing the relationship between amp The app has the following controls: - The OK button adjusts the width of the spectrum. +- A long press of the OK button toggles modulation (default/narrow). - The Up and Down buttons zoom in and out. - The Left and Right buttons switch between different frequency bands. -The last detected peak stays on screen (prefixed "Last:") until a new peak is found, so short bursts are easy to read even after the signal is gone. +The last detected peak stays on screen (prefixed "Last:" once the signal is gone) until a new peak is found, so short bursts are easy to read after the fact. The app saves the current view (frequency, spectrum width, modulation and zoom) on exit and restores it on the next launch. diff --git a/base_pack/spectrum_analyzer/spectrum_analyzer.c b/base_pack/spectrum_analyzer/spectrum_analyzer.c index 4c1d5b6f..37645662 100644 --- a/base_pack/spectrum_analyzer/spectrum_analyzer.c +++ b/base_pack/spectrum_analyzer/spectrum_analyzer.c @@ -184,7 +184,7 @@ static void spectrum_analyzer_render_callback(Canvas* const canvas, void* ctx) { canvas_draw_str_aligned(canvas, 127, 4, AlignRight, AlignTop, tmp_str); } - // Draw cross and label + // Draw cross on the current peak 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); @@ -213,7 +213,8 @@ static void spectrum_analyzer_render_callback(Canvas* const canvas, void* ctx) { canvas_draw_line(canvas, (uint8_t)x1, (uint8_t)y1, (uint8_t)x2, (uint8_t)y2); } - // Label: keep showing the last locked peak even after the signal is gone + // Peak label: latched, so it survives after the signal is gone; hidden + // while the mode/modulation overlay occupies the top-right corner if((model->last_peak_rssi > PEAK_THRESHOLD) && !model->mode_change && !model->modulation_change) { char temp_str[36]; @@ -245,6 +246,7 @@ static void spectrum_analyzer_worker_callback( float max_rssi, uint8_t max_rssi_dec, uint8_t max_rssi_channel, + uint32_t max_rssi_frequency, void* context) { SpectrumAnalyzer* spectrum_analyzer = context; furi_check( @@ -256,12 +258,11 @@ static void spectrum_analyzer_worker_callback( model->max_rssi_dec = max_rssi_dec; model->max_rssi_channel = max_rssi_channel; - // Remember the last peak, frequency is stored in Hz so it stays - // valid after the view is retuned + // Latch the peak with the absolute frequency (Hz) the worker measured + // it at, so the held label stays correct after the view is retuned if(max_rssi > PEAK_THRESHOLD) { model->last_peak_rssi = max_rssi; - model->last_peak_frequency = - model->channel0_frequency + (max_rssi_channel * model->spacing); + model->last_peak_frequency = max_rssi_frequency; } furi_mutex_release(spectrum_analyzer->model_mutex); @@ -410,7 +411,7 @@ void spectrum_analyzer_calculate_frequencies(SpectrumAnalyzerModel* model) { } static void spectrum_analyzer_load_settings(SpectrumAnalyzerModel* model) { - SpectrumAnalyzerSettings settings; + SpectrumAnalyzerSettings settings = {0}; if(!saved_struct_load( SPECTRUM_ANALYZER_SETTINGS_PATH, &settings, @@ -420,10 +421,12 @@ static void spectrum_analyzer_load_settings(SpectrumAnalyzerModel* model) { return; } - // Ignore saved values that are out of range, keep defaults instead + // If any saved value is out of range, discard the whole file and keep + // all defaults if((settings.center_freq < MIN_300) || (settings.center_freq > MAX_900) || (settings.width > PRECISE) || (settings.modulation > NARROW_MODULATION) || (settings.vscroll > MAX_VSCROLL)) { + FURI_LOG_W("Spectrum", "Discarding out-of-range saved settings"); return; } diff --git a/base_pack/spectrum_analyzer/spectrum_analyzer_worker.c b/base_pack/spectrum_analyzer/spectrum_analyzer_worker.c index c425b2b3..7264781f 100644 --- a/base_pack/spectrum_analyzer/spectrum_analyzer_worker.c +++ b/base_pack/spectrum_analyzer/spectrum_analyzer_worker.c @@ -24,6 +24,7 @@ struct SpectrumAnalyzerWorker { float max_rssi; uint8_t max_rssi_dec; uint8_t max_rssi_channel; + uint32_t max_rssi_frequency; uint8_t channel_ss[NUM_CHANNELS]; }; @@ -211,13 +212,10 @@ static int32_t spectrum_analyzer_worker_thread(void* context) { 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; + uint32_t frequency = instance->channel0_frequency + (ch * instance->spacing); - 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)); + if(subghz_devices_is_frequency_valid(instance->radio_device, frequency)) + subghz_devices_set_frequency(instance->radio_device, frequency); subghz_devices_set_rx(instance->radio_device); furi_delay_ms(3); @@ -233,6 +231,9 @@ static int32_t spectrum_analyzer_worker_thread(void* context) { instance->max_rssi_dec = instance->channel_ss[ch]; instance->max_rssi = (instance->channel_ss[ch] / 2) - 138; instance->max_rssi_channel = ch; + // Capture the frequency this channel was measured at, so a + // concurrent retune cannot mislabel the peak + instance->max_rssi_frequency = frequency; } subghz_devices_idle(instance->radio_device); @@ -247,6 +248,7 @@ static int32_t spectrum_analyzer_worker_thread(void* context) { instance->max_rssi, instance->max_rssi_dec, instance->max_rssi_channel, + instance->max_rssi_frequency, instance->callback_context); } } diff --git a/base_pack/spectrum_analyzer/spectrum_analyzer_worker.h b/base_pack/spectrum_analyzer/spectrum_analyzer_worker.h index 796f532e..8544709b 100644 --- a/base_pack/spectrum_analyzer/spectrum_analyzer_worker.h +++ b/base_pack/spectrum_analyzer/spectrum_analyzer_worker.h @@ -7,6 +7,7 @@ typedef void (*SpectrumAnalyzerWorkerCallback)( float max_rssi, uint8_t max_rssi_dec, uint8_t max_rssi_channel, + uint32_t max_rssi_frequency, void* context); typedef struct SpectrumAnalyzerWorker SpectrumAnalyzerWorker; From d2ea9648fdd70c4f3a89c53d380d6d892b7aabb1 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Thu, 23 Jul 2026 19:34:49 +0300 Subject: [PATCH 3/4] Freq Analyzer [Ext]: reset radio to boot state on worker exit Port of DarkFlippers/unleashed-firmware#1045: the worker parked the active radio (internal or external CC1101) with its custom near-field AGC/BW registers retained through SPWD sleep, and the internal RF switch left isolated. Reset the radio and re-park the RF path on exit so the chip is left in the same state as after boot. Note: this app is not buildable under the standalone ufbt SDK (it depends on firmware-internal headers); verified the failure predates this change against the pristine tree. The touched functions are all already used in this file, so no new API surface is introduced. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../helpers/subghz_frequency_analyzer_worker.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/non_catalog_apps/freq_analyzer_ext/helpers/subghz_frequency_analyzer_worker.c b/non_catalog_apps/freq_analyzer_ext/helpers/subghz_frequency_analyzer_worker.c index cba8df36..a3bc6d61 100644 --- a/non_catalog_apps/freq_analyzer_ext/helpers/subghz_frequency_analyzer_worker.c +++ b/non_catalog_apps/freq_analyzer_ext/helpers/subghz_frequency_analyzer_worker.c @@ -275,6 +275,11 @@ static int32_t subghz_frequency_analyzer_worker_thread(void* context) { // furi_hal_subghz_idle(); // furi_hal_subghz_sleep(); subghz_devices_idle(radio_device); + // Reset drops the analyzer's custom AGC/BW config, which SPWD would + // otherwise retain; SRES reverts the internal radio's IOCFG2, so re-park + // the RF switch - leaves the radio in the same state as after boot + subghz_devices_reset(radio_device); + furi_hal_subghz_set_path(FuriHalSubGhzPathIsolate); subghz_devices_sleep(radio_device); return 0; From eb1a68ed5283aa48a1b559c9d2124116793b3970 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Thu, 23 Jul 2026 19:43:15 +0300 Subject: [PATCH 4/4] Spectrum Analyzer, Freq Analyzer [Ext]: add changelogs Backfilled from git history following the repo convention (see tetris_game, calculator). Freq Analyzer [Ext] never had a fap_version; set it to 1.2, retroactively labeling the 2FSK preset support as 1.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- base_pack/spectrum_analyzer/CHANGELOG.md | 23 +++++++++++++++++++ .../freq_analyzer_ext/CHANGELOG.md | 13 +++++++++++ .../freq_analyzer_ext/application.fam | 1 + 3 files changed, 37 insertions(+) create mode 100644 base_pack/spectrum_analyzer/CHANGELOG.md create mode 100644 non_catalog_apps/freq_analyzer_ext/CHANGELOG.md diff --git a/base_pack/spectrum_analyzer/CHANGELOG.md b/base_pack/spectrum_analyzer/CHANGELOG.md new file mode 100644 index 00000000..e814c074 --- /dev/null +++ b/base_pack/spectrum_analyzer/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## 1.5 + +- Add peak hold: the last detected peak stays on screen ("Peak:" while the signal is live, "Last:" once it is gone) until a new peak replaces it, so short bursts can be read after the fact; the label stores the frequency measured at sweep time, so retuning or zooming cannot mislabel it +- Save the current view (frequency, spectrum width, modulation and vertical scale) on exit and restore it on the next launch, including applying the restored modulation to the radio +- Hide the peak label while the mode/modulation overlay is visible (the two used to overlap in the top-right corner) + +## 1.4 + +- Version bump for catalog compatibility (no functional changes) + +## 1.3 + +- Version bump for catalog compatibility (no functional changes) + +## 1.2 + +- Version bump for catalog compatibility (no functional changes) + +## 1.1 + +- Initial catalog release: CC1101-based spectrum analyzer with five zoom widths, adjustable vertical scale, two modulation presets and external radio module support (original app by @jolcese) diff --git a/non_catalog_apps/freq_analyzer_ext/CHANGELOG.md b/non_catalog_apps/freq_analyzer_ext/CHANGELOG.md new file mode 100644 index 00000000..e4abef6c --- /dev/null +++ b/non_catalog_apps/freq_analyzer_ext/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 1.2 + +- Fix the worker leaving the radio in a degraded state after exit (port of the firmware Frequency Analyzer fix): the active radio (internal or external CC1101) kept the analyzer's near-field AGC/bandwidth registers through sleep; the radio is now reset and the RF path re-parked, leaving the chip in the same state as after boot + +## 1.1 + +- Add support for the 2FSK 12 kHz deviation preset + +## 1.0 + +- Initial release: the firmware's Frequency Analyzer as a standalone app with external CC1101 module support diff --git a/non_catalog_apps/freq_analyzer_ext/application.fam b/non_catalog_apps/freq_analyzer_ext/application.fam index 7668beb4..0baccf68 100644 --- a/non_catalog_apps/freq_analyzer_ext/application.fam +++ b/non_catalog_apps/freq_analyzer_ext/application.fam @@ -13,4 +13,5 @@ App( fap_libs=["assets", "hwdrivers"], fap_icon="icon.png", fap_category="Sub-GHz", + fap_version="1.2", )