diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index d53e7c796..65b3bdf34 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -191,6 +191,7 @@ set(CPPSRC clock_manager.cpp core_control.cpp database.cpp + gradient.cpp rfm69.cpp event_m0.cpp file_reader.cpp diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index 12e118a38..ec4e949ac 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -419,6 +419,17 @@ void FileManBaseView::reload_current(bool reset_pagination) { refresh_list(); } +void FileManBaseView::copy_waterfall(std::filesystem::path path) { + nav_.push( + "Install", " Use this gradient file\n for all waterfalls?", YESNO, + [this, path](bool choice) { + if (choice) { + delete_file(default_gradient_file); + copy_file(path, default_gradient_file); + } + }); +} + const FileManBaseView::file_assoc_t& FileManBaseView::get_assoc( const fs::path& ext) const { size_t index = 0; @@ -685,7 +696,11 @@ bool FileManagerView::handle_file_open() { auto ext = path.extension(); if (path_iequal(txt_ext, ext)) { - nav_.push(path); + if (path_iequal(current_path, u"/" + waterfalls_dir)) { + copy_waterfall(path); + } else { + nav_.push(path); + } return true; } else if (is_cxx_capture_file(path) || path_iequal(ppl_ext, ext)) { // TODO: Enough memory to push? diff --git a/firmware/application/apps/ui_fileman.hpp b/firmware/application/apps/ui_fileman.hpp index e044e53ec..3a4e8da07 100644 --- a/firmware/application/apps/ui_fileman.hpp +++ b/firmware/application/apps/ui_fileman.hpp @@ -96,6 +96,7 @@ class FileManBaseView : public View { void load_directory_contents(const std::filesystem::path& dir_path); void load_directory_contents_unordered(const std::filesystem::path& dir_path, size_t file_cnt); const file_assoc_t& get_assoc(const std::filesystem::path& ext) const; + void copy_waterfall(std::filesystem::path path); NavigationView& nav_; diff --git a/firmware/application/apps/ui_looking_glass_app.cpp b/firmware/application/apps/ui_looking_glass_app.cpp index 5e412c420..2603560a7 100644 --- a/firmware/application/apps/ui_looking_glass_app.cpp +++ b/firmware/application/apps/ui_looking_glass_app.cpp @@ -126,7 +126,7 @@ void GlassView::reset_live_view() { } void GlassView::add_spectrum_pixel(uint8_t power) { - spectrum_row[pixel_index] = spectrum_rgb3_lut[power]; // row of colors + spectrum_row[pixel_index] = gradient.lut[power]; // row of colors spectrum_data[pixel_index] = (live_frequency_integrate * spectrum_data[pixel_index] + power) / (live_frequency_integrate + 1); // smoothing pixel_index++; @@ -359,6 +359,10 @@ GlassView::GlassView( : nav_(nav) { baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum); + if (!gradient.load_file(default_gradient_file)) { + gradient.set_default(); + } + add_children({&labels, &field_frequency_min, &field_frequency_max, diff --git a/firmware/application/apps/ui_looking_glass_app.hpp b/firmware/application/apps/ui_looking_glass_app.hpp index d02803b9e..42d13164b 100644 --- a/firmware/application/apps/ui_looking_glass_app.hpp +++ b/firmware/application/apps/ui_looking_glass_app.hpp @@ -35,7 +35,7 @@ #include "ui_receiver.hpp" #include "string_format.hpp" #include "analog_audio_app.hpp" -#include "spectrum_color_lut.hpp" +#include "gradient.hpp" namespace ui { @@ -74,6 +74,7 @@ class GlassView : public View { private: NavigationView& nav_; + Gradient gradient{}; RxRadioState radio_state_{ReceiverModel::Mode::SpectrumAnalysis}; // Settings rf::Frequency f_min = 260 * MHZ_DIV; // Default to 315/433 remote range. diff --git a/firmware/application/apps/ui_search.cpp b/firmware/application/apps/ui_search.cpp index 73fb5737f..86e041ac1 100644 --- a/firmware/application/apps/ui_search.cpp +++ b/firmware/application/apps/ui_search.cpp @@ -56,6 +56,10 @@ SearchView::SearchView( : nav_(nav) { baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum); + if (!gradient.load_file(default_gradient_file)) { + gradient.set_default(); + } + add_children({&labels, &field_frequency_min, &field_frequency_max, @@ -290,7 +294,7 @@ void SearchView::on_channel_spectrum(const ChannelSpectrum& spectrum) { power = spectrum.db[bin - 128]; } - add_spectrum_pixel(spectrum_rgb3_lut[power]); + add_spectrum_pixel(gradient.lut[power]); mean_acc += power; if (power > max_power) { diff --git a/firmware/application/apps/ui_search.hpp b/firmware/application/apps/ui_search.hpp index c7663e28a..6d7c3afd1 100644 --- a/firmware/application/apps/ui_search.hpp +++ b/firmware/application/apps/ui_search.hpp @@ -24,7 +24,7 @@ #include "receiver_model.hpp" #include "recent_entries.hpp" #include "radio_state.hpp" -#include "spectrum_color_lut.hpp" +#include "gradient.hpp" #include "ui_receiver.hpp" namespace ui { @@ -88,6 +88,7 @@ class SearchView : public View { private: NavigationView& nav_; + Gradient gradient{}; RxRadioState radio_state_{ 100'000'000 /* frequency */, 2500000 /* bandwidth */, diff --git a/firmware/application/file_path.cpp b/firmware/application/file_path.cpp index cea226ec3..f95e7b5b1 100644 --- a/firmware/application/file_path.cpp +++ b/firmware/application/file_path.cpp @@ -51,3 +51,4 @@ const std::filesystem::path whipcalc_dir = u"WHIPCALC"; const std::filesystem::path ook_editor_dir = u"OOKFILES"; const std::filesystem::path hopper_dir = u"HOPPER"; const std::filesystem::path subghz_dir = u"SUBGHZ"; +const std::filesystem::path waterfalls_dir = u"WATERFALLS"; diff --git a/firmware/application/file_path.hpp b/firmware/application/file_path.hpp index d260268af..4c64a86a1 100644 --- a/firmware/application/file_path.hpp +++ b/firmware/application/file_path.hpp @@ -53,5 +53,6 @@ extern const std::filesystem::path whipcalc_dir; extern const std::filesystem::path ook_editor_dir; extern const std::filesystem::path hopper_dir; extern const std::filesystem::path subghz_dir; +extern const std::filesystem::path waterfalls_dir; #endif /* __FILE_PATH_H__ */ diff --git a/firmware/application/gradient.cpp b/firmware/application/gradient.cpp new file mode 100644 index 000000000..2ca27e6bb --- /dev/null +++ b/firmware/application/gradient.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2025 Belousov Oleg + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "gradient.hpp" + +#include "convert.hpp" +#include "file_reader.hpp" +namespace fs = std::filesystem; + +const std::filesystem::path default_gradient_file = u"waterfall.txt"; + +Gradient::Gradient() { + prev_index = 0; + prev_r = 0; + prev_g = 0; + prev_b = 0; +} + +void Gradient::set_default() { + step(86, 0, 0, 255); + step(171, 0, 255, 0); + step(255, 255, 0, 0); +} + +bool Gradient::load_file(const std::filesystem::path& file_path) { + File gradient_file; + auto error = gradient_file.open(file_path.string()); + + if (error) + return false; + + auto reader = FileLineReader(gradient_file); + for (const auto& line : reader) { + if (line.length() == 0 || line[0] == '#') + continue; // Empty or comment line. + + auto cols = split_string(line, ','); + + if (cols.size() == 4) { + int16_t index, r, g, b; + + if (!parse_int(cols[0], index) || index < 0 || index > 255) + continue; + + if (!parse_int(cols[1], r) || r < 0 || r > 255) + continue; + + if (!parse_int(cols[2], g) || g < 0 || g > 255) + continue; + + if (!parse_int(cols[3], b) || b < 0 || b > 255) + continue; + + step(index, r, g, b); + } + } + + return true; +} + +void Gradient::step(int16_t index, int16_t r, int16_t g, int16_t b) { + for (int16_t i = prev_index; i <= index; i++) { + float x = (float)(i - prev_index) / (index - prev_index); + float y = 1.0f - x; + + int16_t new_r = prev_r * y + r * x; + int16_t new_g = prev_g * y + g * x; + int16_t new_b = prev_b * y + b * x; + + lut[i] = ui::Color(new_r, new_g, new_b); + } + + prev_index = index; + prev_r = r; + prev_g = g; + prev_b = b; +} diff --git a/firmware/application/gradient.hpp b/firmware/application/gradient.hpp new file mode 100644 index 000000000..d32d58e59 --- /dev/null +++ b/firmware/application/gradient.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 Belousov Oleg + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GRADIENT_H__ +#define __GRADIENT_H__ + +#include "ui.hpp" +#include "file.hpp" + +#include +#include + +extern const std::filesystem::path default_gradient_file; + +class Gradient { + public: + std::array lut{}; + + Gradient(); + + void set_default(); + bool load_file(const std::filesystem::path& file_path); + + private: + int16_t prev_index = 0; + int16_t prev_r = 0; + int16_t prev_g = 0; + int16_t prev_b = 0; + + void step(int16_t index, int16_t r, int16_t g, int16_t b); +}; + +#endif /* __GRADIENT_H__ */ diff --git a/firmware/application/spectrum_color_lut.cpp b/firmware/application/spectrum_color_lut.cpp index a2877542e..832fa9756 100644 --- a/firmware/application/spectrum_color_lut.cpp +++ b/firmware/application/spectrum_color_lut.cpp @@ -280,265 +280,6 @@ const std::array spectrum_rgb2_lut{{ {132, 0, 0}, }}; -const std::array spectrum_rgb3_lut{{ - {0, 0, 0}, - {0, 0, 3}, - {0, 0, 6}, - {0, 0, 9}, - {0, 0, 12}, - {0, 0, 15}, - {0, 0, 18}, - {0, 0, 21}, - {0, 0, 24}, - {0, 0, 27}, - {0, 0, 30}, - {0, 0, 33}, - {0, 0, 36}, - {0, 0, 39}, - {0, 0, 42}, - {0, 0, 45}, - {0, 0, 48}, - {0, 0, 51}, - {0, 0, 54}, - {0, 0, 57}, - {0, 0, 60}, - {0, 0, 63}, - {0, 0, 66}, - {0, 0, 69}, - {0, 0, 72}, - {0, 0, 75}, - {0, 0, 78}, - {0, 0, 81}, - {0, 0, 84}, - {0, 0, 87}, - {0, 0, 90}, - {0, 0, 93}, - {0, 0, 96}, - {0, 0, 99}, - {0, 0, 102}, - {0, 0, 105}, - {0, 0, 108}, - {0, 0, 111}, - {0, 0, 114}, - {0, 0, 117}, - {0, 0, 120}, - {0, 0, 123}, - {0, 0, 126}, - {0, 0, 129}, - {0, 0, 132}, - {0, 0, 135}, - {0, 0, 138}, - {0, 0, 141}, - {0, 0, 144}, - {0, 0, 147}, - {0, 0, 150}, - {0, 0, 153}, - {0, 0, 156}, - {0, 0, 159}, - {0, 0, 162}, - {0, 0, 165}, - {0, 0, 168}, - {0, 0, 171}, - {0, 0, 174}, - {0, 0, 177}, - {0, 0, 180}, - {0, 0, 183}, - {0, 0, 186}, - {0, 0, 189}, - {0, 0, 192}, - {0, 0, 195}, - {0, 0, 198}, - {0, 0, 201}, - {0, 0, 204}, - {0, 0, 207}, - {0, 0, 210}, - {0, 0, 213}, - {0, 0, 216}, - {0, 0, 219}, - {0, 0, 222}, - {0, 0, 225}, - {0, 0, 228}, - {0, 0, 231}, - {0, 0, 234}, - {0, 0, 237}, - {0, 0, 240}, - {0, 0, 243}, - {0, 0, 246}, - {0, 0, 249}, - {0, 0, 252}, - {0, 0, 255}, - {0, 3, 252}, - {0, 6, 249}, - {0, 9, 246}, - {0, 12, 243}, - {0, 15, 240}, - {0, 18, 237}, - {0, 21, 234}, - {0, 24, 231}, - {0, 27, 228}, - {0, 30, 225}, - {0, 33, 222}, - {0, 36, 219}, - {0, 39, 216}, - {0, 42, 213}, - {0, 45, 210}, - {0, 48, 207}, - {0, 51, 204}, - {0, 54, 201}, - {0, 57, 198}, - {0, 60, 195}, - {0, 63, 192}, - {0, 66, 189}, - {0, 69, 186}, - {0, 72, 183}, - {0, 75, 180}, - {0, 78, 177}, - {0, 81, 174}, - {0, 84, 171}, - {0, 87, 168}, - {0, 90, 165}, - {0, 93, 162}, - {0, 96, 159}, - {0, 99, 156}, - {0, 102, 153}, - {0, 105, 150}, - {0, 108, 147}, - {0, 111, 144}, - {0, 114, 141}, - {0, 117, 138}, - {0, 120, 135}, - {0, 123, 132}, - {0, 126, 129}, - {0, 129, 126}, - {0, 132, 123}, - {0, 135, 120}, - {0, 138, 117}, - {0, 141, 114}, - {0, 144, 111}, - {0, 147, 108}, - {0, 150, 105}, - {0, 153, 102}, - {0, 156, 99}, - {0, 159, 96}, - {0, 162, 93}, - {0, 165, 90}, - {0, 168, 87}, - {0, 171, 84}, - {0, 174, 81}, - {0, 177, 78}, - {0, 180, 75}, - {0, 183, 72}, - {0, 186, 69}, - {0, 189, 66}, - {0, 192, 63}, - {0, 195, 60}, - {0, 198, 57}, - {0, 201, 54}, - {0, 204, 51}, - {0, 207, 48}, - {0, 210, 45}, - {0, 213, 42}, - {0, 216, 39}, - {0, 219, 36}, - {0, 222, 33}, - {0, 225, 30}, - {0, 228, 27}, - {0, 231, 24}, - {0, 234, 21}, - {0, 237, 18}, - {0, 240, 15}, - {0, 243, 12}, - {0, 246, 9}, - {0, 249, 6}, - {0, 252, 3}, - {0, 255, 0}, - {3, 252, 0}, - {6, 249, 0}, - {9, 246, 0}, - {12, 243, 0}, - {15, 240, 0}, - {18, 237, 0}, - {21, 234, 0}, - {24, 231, 0}, - {27, 228, 0}, - {30, 225, 0}, - {33, 222, 0}, - {36, 219, 0}, - {39, 216, 0}, - {42, 213, 0}, - {45, 210, 0}, - {48, 207, 0}, - {51, 204, 0}, - {54, 201, 0}, - {57, 198, 0}, - {60, 195, 0}, - {63, 192, 0}, - {66, 189, 0}, - {69, 186, 0}, - {72, 183, 0}, - {75, 180, 0}, - {78, 177, 0}, - {81, 174, 0}, - {84, 171, 0}, - {87, 168, 0}, - {90, 165, 0}, - {93, 162, 0}, - {96, 159, 0}, - {99, 156, 0}, - {102, 153, 0}, - {105, 150, 0}, - {108, 147, 0}, - {111, 144, 0}, - {114, 141, 0}, - {117, 138, 0}, - {120, 135, 0}, - {123, 132, 0}, - {126, 129, 0}, - {129, 126, 0}, - {132, 123, 0}, - {135, 120, 0}, - {138, 117, 0}, - {141, 114, 0}, - {144, 111, 0}, - {147, 108, 0}, - {150, 105, 0}, - {153, 102, 0}, - {156, 99, 0}, - {159, 96, 0}, - {162, 93, 0}, - {165, 90, 0}, - {168, 87, 0}, - {171, 84, 0}, - {174, 81, 0}, - {177, 78, 0}, - {180, 75, 0}, - {183, 72, 0}, - {186, 69, 0}, - {189, 66, 0}, - {192, 63, 0}, - {195, 60, 0}, - {198, 57, 0}, - {201, 54, 0}, - {204, 51, 0}, - {207, 48, 0}, - {210, 45, 0}, - {213, 42, 0}, - {216, 39, 0}, - {219, 36, 0}, - {222, 33, 0}, - {225, 30, 0}, - {228, 27, 0}, - {231, 24, 0}, - {234, 21, 0}, - {237, 18, 0}, - {240, 15, 0}, - {243, 12, 0}, - {246, 9, 0}, - {249, 6, 0}, - {252, 3, 0}, - {255, 0, 0}, -}}; - const std::array spectrum_rgb4_lut{{ {0, 0, 0}, {1, 1, 1}, diff --git a/firmware/application/spectrum_color_lut.hpp b/firmware/application/spectrum_color_lut.hpp index 1516ae3b1..0e748c33d 100644 --- a/firmware/application/spectrum_color_lut.hpp +++ b/firmware/application/spectrum_color_lut.hpp @@ -27,7 +27,6 @@ #include extern const std::array spectrum_rgb2_lut; -extern const std::array spectrum_rgb3_lut; extern const std::array spectrum_rgb4_lut; #endif /*__SPECTRUM_COLOR_LUT_H__*/ diff --git a/firmware/application/ui/ui_spectrum.cpp b/firmware/application/ui/ui_spectrum.cpp index 8e7e98ca9..93bca6278 100644 --- a/firmware/application/ui/ui_spectrum.cpp +++ b/firmware/application/ui/ui_spectrum.cpp @@ -21,8 +21,6 @@ #include "ui_spectrum.hpp" -#include "spectrum_color_lut.hpp" - #include "portapack.hpp" using namespace portapack; @@ -271,12 +269,12 @@ void WaterfallWidget::on_channel_spectrum( std::array pixel_row; for (size_t i = 0; i < 120; i++) { - const auto pixel_color = spectrum_rgb3_lut[spectrum.db[256 - 120 + i]]; + const auto pixel_color = gradient.lut[spectrum.db[256 - 120 + i]]; pixel_row[i] = pixel_color; } for (size_t i = 120; i < 240; i++) { - const auto pixel_color = spectrum_rgb3_lut[spectrum.db[i - 120]]; + const auto pixel_color = gradient.lut[spectrum.db[i - 120]]; pixel_row[i] = pixel_color; } @@ -305,6 +303,10 @@ WaterfallView::WaterfallView(const bool cursor) { frequency_scale.on_select = [this](int32_t offset) { if (on_select) on_select(offset); }; + + if (!waterfall_widget.gradient.load_file(default_gradient_file)) { + waterfall_widget.gradient.set_default(); + } } void WaterfallView::on_show() { diff --git a/firmware/application/ui/ui_spectrum.hpp b/firmware/application/ui/ui_spectrum.hpp index 9d0d70cb9..398e419ca 100644 --- a/firmware/application/ui/ui_spectrum.hpp +++ b/firmware/application/ui/ui_spectrum.hpp @@ -24,6 +24,7 @@ #include "ui.hpp" #include "ui_widget.hpp" +#include "gradient.hpp" #include "event_m0.hpp" @@ -111,6 +112,8 @@ class FrequencyScale : public Widget { class WaterfallWidget : public Widget { public: + Gradient gradient{}; + void on_show() override; void on_hide() override; void paint(Painter&) override {} diff --git a/sdcard/WATERFALLS/aurora.txt b/sdcard/WATERFALLS/aurora.txt new file mode 100644 index 000000000..f564ebf8e --- /dev/null +++ b/sdcard/WATERFALLS/aurora.txt @@ -0,0 +1,14 @@ +# 0% black +0,0,0,0 + +# 25% blue +64,0,0,255 + +# 50% magenta +128,255,0,255 + +# 75% green +192,0,255,0 + +# 100% white +255,255,255,255 diff --git a/sdcard/WATERFALLS/default.txt b/sdcard/WATERFALLS/default.txt new file mode 100644 index 000000000..8a13db0b3 --- /dev/null +++ b/sdcard/WATERFALLS/default.txt @@ -0,0 +1,14 @@ +# 0% black +0,0,0,0 + +# dark red +86,0,0,255 + +#transparent purple +171,0,255,0 + +#transparent red +255,255,0,0 + +# 100% white +255,255,255,255 diff --git a/sdcard/WATERFALLS/flame.txt b/sdcard/WATERFALLS/flame.txt new file mode 100644 index 000000000..83bdbcc3f --- /dev/null +++ b/sdcard/WATERFALLS/flame.txt @@ -0,0 +1,11 @@ +# 0% black +0,0,0,0 + +# 33% red +84,255,0,0 + +# 66% yellow +168,255,255,0 + +# 100% white +255,255,255,255 diff --git a/sdcard/WATERFALLS/matrix.txt b/sdcard/WATERFALLS/matrix.txt new file mode 100644 index 000000000..726ad89d1 --- /dev/null +++ b/sdcard/WATERFALLS/matrix.txt @@ -0,0 +1,8 @@ +# 0% black +0,0,0,0 + +# 50% green +128,0,255,0 + +# 100% white +255,255,255,255 diff --git a/sdcard/WATERFALLS/sunset.txt b/sdcard/WATERFALLS/sunset.txt new file mode 100644 index 000000000..aeae107d7 --- /dev/null +++ b/sdcard/WATERFALLS/sunset.txt @@ -0,0 +1,14 @@ +# 0% black +0,0,0,0 + +# 25% blue +64,0,0,255 + +# 50% red +128,255,0,0 + +# 75% yellow +192,255,255,0 + +# 100% white +255,255,255,255