diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index ab7052daf..9d7a5db3c 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -222,7 +222,11 @@ set(EXTCPPSRC #gfxEQ 80 byte external/gfxeq/main.cpp - external/gfxeq/ui_gfxeq.cpp + external/gfxeq/ui_gfxeq.cpp + + #waterfall designer + external/waterfall_designer/main.cpp + external/waterfall_designer/ui_waterfall_designer.cpp #detector_rx 168 byte external/detector_rx/main.cpp @@ -289,7 +293,6 @@ set(EXTCPPSRC #morse_radio external/morse_radio/main.cpp external/morse_radio/ui_morse_radio.cpp - ) set(EXTAPPLIST @@ -346,6 +349,7 @@ set(EXTAPPLIST scanner level gfxeq + waterfall_designer detector_rx spaceinv blackjack diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 99a9f2dba..954d59112 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -92,6 +92,7 @@ MEMORY ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k + ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k } SECTIONS @@ -402,6 +403,12 @@ SECTIONS *(*ui*external_app*gfxeq*); } > ram_external_app_gfxeq + .external_app_waterfall_designer : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_waterfall_designer.application_information)); + *(*ui*external_app*waterfall_designer*); + } > ram_external_app_waterfall_designer + .external_app_detector_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_detector_rx.application_information)); @@ -511,6 +518,5 @@ SECTIONS KEEP(*(.external_app.app_morse_radio.application_information)); *(*ui*external_app*morse_radio*); } > ram_external_app_morse_radio - } diff --git a/firmware/application/external/waterfall_designer/main.cpp b/firmware/application/external/waterfall_designer/main.cpp new file mode 100644 index 000000000..dce575fa9 --- /dev/null +++ b/firmware/application/external/waterfall_designer/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Bernd Herzog + * + * 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 "ui.hpp" +#include "ui_waterfall_designer.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::waterfall_designer { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::waterfall_designer + +extern "C" { + +__attribute__((section(".external_app.app_waterfall_designer.application_information"), used)) application_information_t _application_information_waterfall_designer = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::waterfall_designer::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Wt Design", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0xFE, + 0x7F, + 0x03, + 0xC0, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0x03, + 0xC0, + 0xFF, + 0xFF, + 0xFB, + 0xD7, + 0xFE, + 0x7F, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::cyan().v, + /*.menu_location = */ app_location_t::UTILITIES, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'C', 'A', 'P'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp new file mode 100644 index 000000000..e4d3da40c --- /dev/null +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -0,0 +1,503 @@ +/* + * Copyleft Mr. Robot + * Copyright HTotoo + * + * 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 "ui_waterfall_designer.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "ui_freqman.hpp" +#include "file_path.hpp" +#include "ui_fileman.hpp" +#include "file_reader.hpp" +#include "ui_textentry.hpp" + +using namespace portapack; + +namespace ui::external_app::waterfall_designer { + +WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({&labels, + &field_frequency, + &field_frequency_step, + &field_rf_amp, + &field_lna, + &field_vga, + &option_bandwidth, + &record_view, + &menu_view, + &button_new, + &button_open, + &button_save, + &button_add_level, + &button_remove_level, + &button_edit_color, + &button_apply_setting, + &waterfall}); + ensure_directory(waterfalls_dir); + ui::Rect waterfall_rect{0, header_height, screen_rect().width(), screen_rect().height() - header_height}; + waterfall.set_parent_rect(waterfall_rect); + + menu_view.set_parent_rect({0, 1 * 16, screen_width, 7 * 16}); + + field_frequency_step.set_by_value(receiver_model.frequency_step()); + field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) { + receiver_model.set_frequency_step(v); + this->field_frequency.set_step(v); + }; + + freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth); + option_bandwidth.on_change = [this](size_t, uint32_t new_capture_rate) { + /* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC + * provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */ + + /* capture_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */ + /* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */ + /* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */ + + waterfall.stop(); + + // record_view determines the correct oversampling to apply and returns the actual sample rate. + // NB: record_view is what actually updates proc_capture baseband settings. + auto actual_sample_rate = record_view.set_sampling_rate(new_capture_rate); + + // Update the radio model with the actual sampling rate. + receiver_model.set_sampling_rate(actual_sample_rate); + + // Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate. + auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate); + receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth); + + capture_rate = new_capture_rate; + + waterfall.start(); + }; + + button_new.on_select = [this]() { + on_create_new_profile(); + }; + + button_open.on_select = [this]() { + on_open_profile(); + }; + + button_save.on_select = [this]() { + on_save_profile(); + }; + + button_add_level.on_select = [this]() { + on_add_level(); + }; + + button_remove_level.on_select = [this]() { + on_remove_level(); + }; + + button_edit_color.on_select = [this]() { + on_edit_color(); + }; + + button_apply_setting.on_select = [this]() { + if_apply_setting = true; + on_apply_current_to_wtf(); + // nav_.pop(); + }; + + menu_view.on_highlight = [this]() { + highlighted_index_ = menu_view.highlighted_index(); + }; + + receiver_model.enable(); + option_bandwidth.set_by_value(capture_rate); + + record_view.on_error = [&nav](std::string message) { + nav.display_modal("Error", message); + }; + + button_save.hidden(true); + button_add_level.hidden(true); + button_remove_level.hidden(true); + button_edit_color.hidden(true); + button_apply_setting.hidden(true); + + refresh_menu_view(); +} + +WaterfallDesignerView::WaterfallDesignerView( + NavigationView& nav, + ReceiverModel::settings_t override) + : WaterfallDesignerView(nav) { + // Settings to override when launched from another app (versus from AppSettings .ini file) + field_frequency.set_value(override.frequency_app_override); +} + +WaterfallDesignerView::~WaterfallDesignerView() { + if (!if_apply_setting) restore_current_profile(); + receiver_model.disable(); + baseband::shutdown(); +} + +void WaterfallDesignerView::set_parent_rect(const Rect new_parent_rect) { + View::set_parent_rect(new_parent_rect); + + ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height}; + waterfall.set_parent_rect(waterfall_rect); +} + +void WaterfallDesignerView::focus() { + button_open.focus(); +} + +void WaterfallDesignerView::on_freqchg(int64_t freq) { + field_frequency.set_value(freq); +} + +void WaterfallDesignerView::on_open_profile() { + auto open_view = nav_.push(".txt"); + open_view->push_dir(waterfalls_dir); + open_view->on_changed = [this](std::filesystem::path new_file_path) { + on_profile_changed(new_file_path); + }; +} + +void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) { + current_profile_path = new_profile_path; + profile_levels.clear(); + + File playlist_file; + auto error = playlist_file.open(new_profile_path.string()); + + if (error) return; + + menu_view.clear(); + auto reader = FileLineReader(playlist_file); + + for (const auto& line : reader) { + profile_levels.push_back(line); + } + + for (auto& line : profile_levels) { + // remove empty lines + if (line == "\n" || line == "\r\n" || line == "\r") { + profile_levels.erase(std::remove(profile_levels.begin(), profile_levels.end(), line), profile_levels.end()); + } + + // remove line end \n etc + if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) { + line = line.substr(0, line.length() - 1); + } + } + + button_save.hidden(false); + button_add_level.hidden(false); + button_remove_level.hidden(false); + button_edit_color.hidden(false); + button_apply_setting.hidden(false); + + refresh_menu_view(); + on_apply_current_to_wtf(); +} + +void WaterfallDesignerView::refresh_menu_view() { + menu_view.clear(); + + for (const auto& line : profile_levels) { + if (line.length() == 0 || line[0] == '#') { + menu_view.add_item({line, + ui::Color::grey(), + &bitmap_icon_notepad, + [this](KeyEvent) { + button_add_level.focus(); + }}); + } else { + // index,R,G,B + size_t pos = 0; + size_t next_pos = 0; + + // pass index + next_pos = line.find(',', pos); + if (next_pos == std::string::npos) continue; + pos = next_pos + 1; + + // r + next_pos = line.find(',', pos); + if (next_pos == std::string::npos) continue; + uint8_t r = static_cast(std::stoi(line.substr(pos, next_pos - pos))); + pos = next_pos + 1; + + // g + next_pos = line.find(',', pos); + if (next_pos == std::string::npos) continue; + uint8_t g = static_cast(std::stoi(line.substr(pos, next_pos - pos))); + pos = next_pos + 1; + + // b + uint8_t b = static_cast(std::stoi(line.substr(pos))); + + ui::Color color = ui::Color(r, g, b); + menu_view.add_item({line, + color, + &bitmap_icon_cwgen, + [this](KeyEvent) { + button_remove_level.focus(); + }}); + } + } + set_dirty(); +} + +void WaterfallDesignerView::on_apply_current_to_wtf() { + std::filesystem::path system_read_path = "waterfall.txt"; + copy_file(current_profile_path, system_read_path); + + waterfall.load_gradient(); + + set_dirty(); +} + +void WaterfallDesignerView::on_save_profile() { + if (current_profile_path.empty()) { + nav_.display_modal("Err", "No profile file loaded"); + return; + } else if (profile_levels.empty()) { + nav_.display_modal("Err", "List is empty"); + return; + } + + File profile_file; + auto error = profile_file.open(current_profile_path.string(), false, false); + + if (error) { + nav_.display_modal("Err", "open err"); + return; + } + + // clear file + profile_file.seek(0); + profile_file.truncate(); + + // write new data + for (const auto& entry : profile_levels) { + profile_file.write_line(entry); + } + + nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string()); +} + +void WaterfallDesignerView::on_add_level() { + if (highlighted_index_ >= profile_levels.size()) return; + if (profile_levels[highlighted_index_].empty()) return; + if (profile_levels[highlighted_index_][0] == '#') return; + if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + size_t insert_pos = highlighted_index_; + std::string new_entry = "0,128,128,128"; + profile_levels.insert(profile_levels.begin() + insert_pos, new_entry); + refresh_menu_view(); + on_edit_color(); +} + +void WaterfallDesignerView::on_remove_level() { + if (highlighted_index_ >= profile_levels.size()) return; + if (profile_levels[highlighted_index_].empty()) return; + if (profile_levels[highlighted_index_][0] == '#') return; + if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + profile_levels.erase(profile_levels.begin() + highlighted_index_); + refresh_menu_view(); +} + +void WaterfallDesignerView::on_edit_color() { + if (highlighted_index_ >= profile_levels.size()) return; + if (profile_levels[highlighted_index_].empty()) return; + if (profile_levels[highlighted_index_][0] == '#') return; + if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + + auto color_picker_view = nav_.push(profile_levels[highlighted_index_]); + color_picker_view->on_save = [this](std::string new_color) { + profile_levels[highlighted_index_] = new_color; + refresh_menu_view(); + on_apply_current_to_wtf(); + }; +} + +void WaterfallDesignerView::backup_current_profile() { + std::filesystem::path curren_wtf_path = "waterfall.txt"; + std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; + copy_file(curren_wtf_path, backup_path); +} + +void WaterfallDesignerView::restore_current_profile() { + std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; + std::filesystem::path put_back_path = "waterfall.txt"; + copy_file(backup_path, put_back_path); + delete_file(backup_path); +} + +void WaterfallDesignerView::on_apply_setting() { +} + +void WaterfallDesignerView::on_create_new_profile() { + text_prompt( + nav_, + file_name_buffer, + 32 - 4, // fat32 + ENTER_KEYBOARD_MODE_ALPHA, + [this](std::string& buffer) { + if (buffer.empty()) return; + + if (buffer.length() < 4 || buffer.substr(buffer.length() - 4) != ".txt") { + buffer += ".txt"; + } + + File new_file; + auto error = new_file.create(waterfalls_dir / buffer); + if (error) { + nav_.display_modal("Err", "create file err"); + return; + } + + profile_levels.clear(); + current_profile_path = waterfalls_dir / buffer; + on_profile_changed(current_profile_path); + }); +} + +WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str) + : nav_{nav}, + color_str_{color_str} { + add_children({&labels, + &field_red, + &field_green, + &field_blue, + &field_step, + &field_index, + &progressbar, + &button_save}); + + progressbar.set_max(UINT8_MAX); + + size_t pos = 0; + size_t next_pos = 0; + + // index + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + index_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); + pos = next_pos + 1; + } + + // r + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + red_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); + pos = next_pos + 1; + } + + // g + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + green_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); + pos = next_pos + 1; + } + + // b + blue_ = static_cast(std::stoi(color_str.substr(pos))); + + field_red.set_value(red_); + field_green.set_value(green_); + field_blue.set_value(blue_); + field_index.set_value(index_); + + // cb + field_red.on_change = [this](int32_t) { + update_color_index(); + }; + + field_green.on_change = [this](int32_t) { + update_color_index(); + }; + + field_blue.on_change = [this](int32_t) { + update_color_index(); + }; + + button_save.on_select = [this](Button&) { + if (on_save) on_save(build_color_str()); + nav_.pop(); + }; + + field_index.on_change = [this](int32_t) { + update_color_index(); + }; + + field_step.on_change = [this](int32_t) { + field_red.set_step(field_step.value()); + field_green.set_step(field_step.value()); + field_blue.set_step(field_step.value()); + field_index.set_step(field_step.value()); + }; + + update_color_index(); +} + +void WaterfallDesignerColorPickerView::focus() { + button_save.focus(); +} + +void WaterfallDesignerColorPickerView::update_color_index() { + index_ = static_cast(field_index.value()); + red_ = static_cast(field_red.value()); + green_ = static_cast(field_green.value()); + blue_ = static_cast(field_blue.value()); + + const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40}; + + Painter painter_instance_2; + painter_instance_2.fill_rectangle( + {preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()}, + ui::Color(red_, green_, blue_)); +} + +void WaterfallDesignerColorPickerView::paint(Painter& painter) { + // this is not duplicated code. + // because need to display color when enter, + // but it is too early to call update_color() in the constructor. + + const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40}; + + painter.fill_rectangle( + {preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()}, + ui::Color(red_, green_, blue_)); +} + +std::string WaterfallDesignerColorPickerView::build_color_str() { + size_t index_pos = color_str_.find(','); + if (index_pos != std::string::npos) { + return color_str_.substr(0, index_pos + 1) + + to_string_dec_uint(red_) + "," + + to_string_dec_uint(green_) + "," + + to_string_dec_uint(blue_); + } + return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_); +} + +} /* namespace ui::external_app::waterfall_designer */ diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp new file mode 100644 index 000000000..e1fadf5fe --- /dev/null +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -0,0 +1,265 @@ +/* + * Copyleft Mr. Robot + * Copyright HTotoo + * + * 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 __WATERFALL_DESIGNER_APP_HPP__ +#define __WATERFALL_DESIGNER_APP_HPP__ + +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "ui_record_view.hpp" +#include "ui_spectrum.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "file_path.hpp" + +namespace ui::external_app::waterfall_designer { + +enum class ColorComponent { + RED, + GREEN, + BLUE +}; + +class WaterfallDesignerColorPickerView : public View { + public: + std::function on_save{}; + + WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str); + + std::string title() const override { return "Color Picker"; }; + void focus() override; + void paint(Painter& painter) override; + + private: + NavigationView& nav_; + std::string color_str_; + uint8_t index_{0}; + uint8_t red_{0}; + uint8_t green_{0}; + uint8_t blue_{0}; + + void update_color_index(); + std::string build_color_str(); + + Labels labels{ + {{0 * 8, 0 * 16}, "Index", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 2 * 16}, "Red", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 4 * 16}, "Green", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 6 * 16}, "Blue", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 8 * 16}, "Step", Theme::getInstance()->fg_light->foreground}}; + + NumberField field_index{ + {0 * 8, 1 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_red{ + {0 * 8, 3 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_green{ + {0 * 8, 5 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_blue{ + {0 * 8, 7 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_step{ + {0 * 8, 9 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + ProgressBar progressbar{ + {0 * 8, + screen_height - 4 * 16 - 2 * 16 - 1 * 16, + screen_width, + 2 * 16}}; + + Button button_save{ + {0, screen_height - 4 * 16, screen_width, 4 * 16}, + "Save"}; +}; + +class WaterfallDesignerView : public View { + public: + WaterfallDesignerView(NavigationView& nav); + WaterfallDesignerView(NavigationView& nav, ReceiverModel::settings_t override); + ~WaterfallDesignerView(); + + void focus() override; + void set_parent_rect(const Rect new_parent_rect) override; + + std::string title() const override { return "Wtf Design"; }; + + private: + uint32_t capture_rate{500000}; + uint32_t file_format{0}; + uint32_t highlighted_index_{0}; + bool trim{false}; + std::filesystem::path current_profile_path = ""; + NavigationView& nav_; + RxRadioState radio_state_{ReceiverModel::Mode::Capture}; + app_settings::SettingsManager settings_{ + "rx_wtf_designer", + app_settings::Mode::RX, + { + {"capture_rate"sv, &capture_rate}, + {"file_format"sv, &file_format}, + {"trim"sv, &trim}, + }}; + + Labels labels{ + {{0 * 8, 1 * 16}, "Rate:", Theme::getInstance()->fg_light->foreground}, + {{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground}, + }; + + RxFrequencyField field_frequency{ + {0 * 8, 0 * 16}, + nav_}; + + FrequencyStepView field_frequency_step{ + {10 * 8, 0 * 16}}; + + RFAmpField field_rf_amp{ + {16 * 8, 0 * 16}}; + + LNAGainField field_lna{ + {18 * 8, 0 * 16}}; + + VGAGainField field_vga{ + {21 * 8, 0 * 16}}; + + OptionsField option_bandwidth{ + {24 * 8, 0 * 16}, + 5, + {}}; + + MenuView menu_view{}; + + NewButton button_new{ + {0 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_file, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_open{ + {4 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_load, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_save{ + {8 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_save, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_add_level{ + {12 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_add, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_remove_level{ + {16 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_delete, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_edit_color{ + {20 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_notepad, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_apply_setting{ + {24 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_replay, + Theme::getInstance()->fg_blue->foreground}; + + void backup_current_profile(); + void restore_current_profile(); + void on_create_new_profile(); + void on_open_profile(); + void on_profile_changed(std::filesystem::path new_profile_path); + void on_save_profile(); + void on_add_level(); + void on_remove_level(); + void on_edit_color(); + + void refresh_menu_view(); + + void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct + void on_apply_setting(); // apply set + + bool if_apply_setting{false}; + /*NB: + this works as: + each time you change color, it apply as file realtime + however if you don't push the apply (play) btn, it would resotore in distructor, + if you push apply, it would apply and exit*/ + + std::vector profile_levels{}; + + static constexpr ui::Dim header_height = 10 * 16; + + RecordView record_view{// we still need it cuz it make waterfall correct + {screen_width, screen_height, 30 * 8, 1 * 16}, + u"BBD_????.*", + captures_dir, + RecordView::FileType::RawS16, + 16384, + 3}; + + spectrum::WaterfallView waterfall{}; + std::string file_name_buffer{}; // needed by text_prompy + + MessageHandlerRegistration message_handler_freqchg{ + Message::ID::FreqChangeCommand, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freqchg(message->freq); + }}; + + void on_freqchg(int64_t freq); +}; + +} /* namespace ui::external_app::waterfall_designer */ + +#endif /*__WATERFALL_DESIGNER_APP_HPP__*/ diff --git a/firmware/application/ui/ui_spectrum.cpp b/firmware/application/ui/ui_spectrum.cpp index f548c542b..0fcb8c504 100644 --- a/firmware/application/ui/ui_spectrum.cpp +++ b/firmware/application/ui/ui_spectrum.cpp @@ -332,6 +332,10 @@ WaterfallView::WaterfallView(const bool cursor) { } }; + load_gradient(); +} + +void WaterfallView::load_gradient() { if (!waterfall_widget.gradient.load_file(default_gradient_file)) { waterfall_widget.gradient.set_default(); } diff --git a/firmware/application/ui/ui_spectrum.hpp b/firmware/application/ui/ui_spectrum.hpp index 9b8cf3ffc..ecc8988a8 100644 --- a/firmware/application/ui/ui_spectrum.hpp +++ b/firmware/application/ui/ui_spectrum.hpp @@ -146,6 +146,7 @@ class WaterfallView : public View { void set_parent_rect(const Rect new_parent_rect) override; void show_audio_spectrum_view(const bool show); + void load_gradient(); private: void update_widgets_rect();