From e8fcb5057215dbe2a3fcc88ba89e62e929f9c9e5 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 15:40:39 +0800 Subject: [PATCH 1/9] copy capture --- firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 7 + .../external/waterfall_designer/main.cpp | 83 +++++++++++ .../ui_waterfall_designer.cpp | 140 ++++++++++++++++++ .../ui_waterfall_designer.hpp | 133 +++++++++++++++++ 5 files changed, 368 insertions(+) create mode 100644 firmware/application/external/waterfall_designer/main.cpp create mode 100644 firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp create mode 100644 firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 66a9ceb3e..4bb69f210 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -207,6 +207,10 @@ set(EXTCPPSRC #gfxEQ external/gfxeq/main.cpp external/gfxeq/ui_gfxeq.cpp + + #waterfall designer + external/waterfall_designer/main.cpp + external/waterfall_designer/ui_waterfall_designer.cpp ) set(EXTAPPLIST @@ -260,4 +264,5 @@ set(EXTAPPLIST scanner level gfxeq + waterfall_designer ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 4f03965f0..91d53203e 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -73,6 +73,7 @@ MEMORY ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k ram_external_app_level (rwx) : org = 0xADE10000, len = 32k ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k + ram_external_app_waterfall_designer (rwx) : org = 0xADE30000, len = 32k } SECTIONS @@ -375,4 +376,10 @@ SECTIONS KEEP(*(.external_app.app_gfxeq.application_information)); *(*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 } diff --git a/firmware/application/external/waterfall_designer/main.cpp b/firmware/application/external/waterfall_designer/main.cpp new file mode 100644 index 000000000..2b65b0967 --- /dev/null +++ b/firmware/application/external/waterfall_designer/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Mark Thompson + * + * 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 = */ "Wtf Design", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xC0, + 0x01, + 0x80, + 0x00, + 0x80, + 0x20, + 0x60, + 0x13, + 0x10, + 0x0C, + 0x88, + 0x08, + 0x84, + 0x10, + 0x84, + 0x10, + 0xC2, + 0x21, + 0x84, + 0x10, + 0x04, + 0x10, + 0x08, + 0x08, + 0x10, + 0x04, + 0x60, + 0x03, + 0x80, + 0x00, + }, + /*.icon_color = */ ui::Color::cyan().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ 0, + + /*.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..9c8ea0a5b --- /dev/null +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2018 Furrtek + * + * 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" + +using namespace portapack; + +namespace ui::external_app::waterfall_designer { + +WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) + : nav_{nav} { + baseband::run_image(portapack::spi_flash::image_tag_capture); + + add_children({ + &labels, + &rssi, + &channel, + &field_frequency, + &field_frequency_step, + &field_rf_amp, + &field_lna, + &field_vga, + &option_bandwidth, + &option_format, + &check_trim, + &record_view, + &waterfall, + }); + + 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); + }; + + option_format.set_selected_index(file_format); + option_format.on_change = [this](size_t, uint32_t file_type) { + file_format = file_type; + record_view.set_file_type((RecordView::FileType)file_type); + }; + + check_trim.set_value(trim); + check_trim.on_select = [this](Checkbox&, bool v) { + trim = v; + record_view.set_auto_trim(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); + + // Automatically switch default capture format to C8 when bandwidth setting is increased to >=1.5MHz anb back to C16 for <=1,25Mhz + if ((new_capture_rate >= 1500000) && (capture_rate < 1500000)) { + option_format.set_selected_index(1); // Default C8 format for REC, 1500K ... 5500k + } + if ((new_capture_rate <= 1250000) && (capture_rate > 1250000)) { + option_format.set_selected_index(0); // Default C16 format for REC , 12k5 ... 1250K + } + capture_rate = new_capture_rate; + + waterfall.start(); + }; + + receiver_model.enable(); + option_bandwidth.set_by_value(capture_rate); + + record_view.on_error = [&nav](std::string message) { + nav.display_modal("Error", message); + }; +} + +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() { + 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() { + record_view.focus(); +} + +void WaterfallDesignerView::on_freqchg(int64_t freq) { + field_frequency.set_value(freq); +} + +} /* 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..8642dd311 --- /dev/null +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2018 Furrtek + * + * 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 { + +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 "Capture"; }; + + private: + static constexpr ui::Dim header_height = 3 * 16; + + uint32_t capture_rate{500000}; + uint32_t file_format{0}; + bool trim{false}; + + NavigationView& nav_; + RxRadioState radio_state_{ReceiverModel::Mode::Capture}; + app_settings::SettingsManager settings_{ + "rx_capture", + 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}, + }; + + RSSI rssi{ + {24 * 8, 0, 6 * 8, 4}}; + + Channel channel{ + {24 * 8, 5, 6 * 8, 4}}; + + 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{ + {5 * 8, 1 * 16}, + 5, + {}}; + + OptionsField option_format{ + {18 * 8, 1 * 16}, + 3, + {{"C16", RecordView::FileType::RawS16}, + {"C8", RecordView::FileType::RawS8}}}; + + Checkbox check_trim{ + {23 * 8, 1 * 16}, + 4, + "Trim", + /*small*/ true}; + + RecordView record_view{ + {0 * 8, 2 * 16, 30 * 8, 1 * 16}, + u"BBD_????.*", + captures_dir, + RecordView::FileType::RawS16, + 16384, + 3}; + + spectrum::WaterfallView waterfall{}; + + 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__*/ From d071ba5cef94cd00c555fbbea25567795fdf7fbc Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 15:44:55 +0800 Subject: [PATCH 2/9] remove some useless widgets --- .../external/waterfall_designer/main.cpp | 2 +- .../ui_waterfall_designer.cpp | 21 +------------------ .../ui_waterfall_designer.hpp | 13 +----------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/firmware/application/external/waterfall_designer/main.cpp b/firmware/application/external/waterfall_designer/main.cpp index 2b65b0967..f21be3f3c 100644 --- a/firmware/application/external/waterfall_designer/main.cpp +++ b/firmware/application/external/waterfall_designer/main.cpp @@ -38,7 +38,7 @@ __attribute__((section(".external_app.app_waterfall_designer.application_informa /*.header_version = */ CURRENT_HEADER_VERSION, /*.app_version = */ VERSION_MD5, - /*.app_name = */ "Wtf Design", + /*.app_name = */ "Wt Design", /*.bitmap_data = */ { 0x00, 0x00, diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index 9c8ea0a5b..f0059bf2d 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -43,8 +43,6 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) &field_lna, &field_vga, &option_bandwidth, - &option_format, - &check_trim, &record_view, &waterfall, }); @@ -55,17 +53,6 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) this->field_frequency.set_step(v); }; - option_format.set_selected_index(file_format); - option_format.on_change = [this](size_t, uint32_t file_type) { - file_format = file_type; - record_view.set_file_type((RecordView::FileType)file_type); - }; - - check_trim.set_value(trim); - check_trim.on_select = [this](Checkbox&, bool v) { - trim = v; - record_view.set_auto_trim(v); - }; freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth); option_bandwidth.on_change = [this](size_t, uint32_t new_capture_rate) { @@ -89,13 +76,7 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate); receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth); - // Automatically switch default capture format to C8 when bandwidth setting is increased to >=1.5MHz anb back to C16 for <=1,25Mhz - if ((new_capture_rate >= 1500000) && (capture_rate < 1500000)) { - option_format.set_selected_index(1); // Default C8 format for REC, 1500K ... 5500k - } - if ((new_capture_rate <= 1250000) && (capture_rate > 1250000)) { - option_format.set_selected_index(0); // Default C16 format for REC , 12k5 ... 1250K - } + capture_rate = new_capture_rate; waterfall.start(); diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp index 8642dd311..95eb44fb3 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -44,7 +44,7 @@ class WaterfallDesignerView : public View { void focus() override; void set_parent_rect(const Rect new_parent_rect) override; - std::string title() const override { return "Capture"; }; + std::string title() const override { return "Wtf Design"; }; private: static constexpr ui::Dim header_height = 3 * 16; @@ -96,17 +96,6 @@ class WaterfallDesignerView : public View { 5, {}}; - OptionsField option_format{ - {18 * 8, 1 * 16}, - 3, - {{"C16", RecordView::FileType::RawS16}, - {"C8", RecordView::FileType::RawS8}}}; - - Checkbox check_trim{ - {23 * 8, 1 * 16}, - 4, - "Trim", - /*small*/ true}; RecordView record_view{ {0 * 8, 2 * 16, 30 * 8, 1 * 16}, From 4082ed3d540aeb7085b3af283a0572f8c80dc000 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 16:03:21 +0800 Subject: [PATCH 3/9] hide record view by worksround --- .../external/waterfall_designer/ui_waterfall_designer.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp index 95eb44fb3..058a53bf8 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -97,8 +97,8 @@ class WaterfallDesignerView : public View { {}}; - RecordView record_view{ - {0 * 8, 2 * 16, 30 * 8, 1 * 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, From 2dd79c441caecd65406903d73252599839fc62dd Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 16:09:23 +0800 Subject: [PATCH 4/9] remove channel --- .../external/waterfall_designer/ui_waterfall_designer.cpp | 3 +-- .../external/waterfall_designer/ui_waterfall_designer.hpp | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index f0059bf2d..96096640c 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -35,8 +35,7 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) add_children({ &labels, - &rssi, - &channel, + // &channel, &field_frequency, &field_frequency_step, &field_rf_amp, diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp index 058a53bf8..49e5229ed 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -69,11 +69,9 @@ class WaterfallDesignerView : public View { {{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground}, }; - RSSI rssi{ - {24 * 8, 0, 6 * 8, 4}}; - Channel channel{ - {24 * 8, 5, 6 * 8, 4}}; + // Channel channel{ + // {24 * 8, 5, 6 * 8, 4}}; RxFrequencyField field_frequency{ {0 * 8, 0 * 16}, From d0409af4940a1ec471d89b1c3afeff99d5e6196b Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 16:09:43 +0800 Subject: [PATCH 5/9] remove channel --- .../external/waterfall_designer/ui_waterfall_designer.cpp | 1 - .../external/waterfall_designer/ui_waterfall_designer.hpp | 3 --- 2 files changed, 4 deletions(-) diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index 96096640c..4b6e8ffbc 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -35,7 +35,6 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) add_children({ &labels, - // &channel, &field_frequency, &field_frequency_step, &field_rf_amp, diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp index 49e5229ed..30fe33b71 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -70,9 +70,6 @@ class WaterfallDesignerView : public View { }; - // Channel channel{ - // {24 * 8, 5, 6 * 8, 4}}; - RxFrequencyField field_frequency{ {0 * 8, 0 * 16}, nav_}; From 0a5769e8f5bf2f7cb00033da30eb655f1e1340e0 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 18:17:54 +0800 Subject: [PATCH 6/9] todo: color picker --- .../ui_waterfall_designer.cpp | 217 +++++++++++++++++- .../ui_waterfall_designer.hpp | 84 +++++-- firmware/application/ui_navigation.cpp | 4 +- 3 files changed, 283 insertions(+), 22 deletions(-) diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index 4b6e8ffbc..37666b7d2 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -24,6 +24,9 @@ #include "baseband_api.hpp" #include "portapack.hpp" #include "ui_freqman.hpp" +#include "file_path.hpp" +#include "ui_fileman.hpp" +#include "file_reader.hpp" using namespace portapack; @@ -42,16 +45,26 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) &field_vga, &option_bandwidth, &record_view, - &waterfall, + &menu_view, + &button_new, + &button_open, + &button_save, + &button_add_level, + &button_remove_level, + &button_apply_setting, }); + waterfall = std::make_unique(); + add_child(waterfall.get()); + + 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 @@ -61,7 +74,7 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) /* 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(); + 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. @@ -74,10 +87,37 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) 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(); + waterfall->start(); + }; + + button_new.on_select = [this]() { + profile_levels.clear(); + current_profile_path = ""; + refresh_menu_view(); + }; + + 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_apply_setting.on_select = [this]() { + if_apply_setting = true; + on_apply_current_to_wtf(); + nav_.pop(); }; receiver_model.enable(); @@ -86,6 +126,8 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) record_view.on_error = [&nav](std::string message) { nav.display_modal("Error", message); }; + + refresh_menu_view(); } WaterfallDesignerView::WaterfallDesignerView( @@ -97,6 +139,7 @@ WaterfallDesignerView::WaterfallDesignerView( } WaterfallDesignerView::~WaterfallDesignerView() { + if(!if_apply_setting) restore_current_profile(); receiver_model.disable(); baseband::shutdown(); } @@ -105,15 +148,175 @@ 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); + waterfall->set_parent_rect(waterfall_rect); } void WaterfallDesignerView::focus() { - record_view.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); + } + } + + refresh_menu_view(); +} + +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 current_path = "waterfall.txt"; + copy_file(current_profile_path, current_path); + + remove_child(waterfall.get()); + waterfall.reset(); + waterfall = std::make_unique(); + add_child(waterfall.get()); + + ui::Rect waterfall_rect{0, header_height, screen_rect().width(), screen_rect().height() - header_height}; + waterfall->set_parent_rect(waterfall_rect); + + 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() { + // new view to let user add +} + +void WaterfallDesignerView::on_remove_level() { + // remove entrance from vec + profile_levels.erase(profile_levels.begin() + menu_view.highlighted_index()); + refresh_menu_view(); +} + +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() { + +} + + + } /* 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 index 30fe33b71..ece60ab5e 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -47,12 +47,10 @@ class WaterfallDesignerView : public View { std::string title() const override { return "Wtf Design"; }; private: - static constexpr ui::Dim header_height = 3 * 16; - uint32_t capture_rate{500000}; uint32_t file_format{0}; bool trim{false}; - + std::filesystem::path current_profile_path = ""; NavigationView& nav_; RxRadioState radio_state_{ReceiverModel::Mode::Capture}; app_settings::SettingsManager settings_{ @@ -69,7 +67,6 @@ class WaterfallDesignerView : public View { {{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground}, }; - RxFrequencyField field_frequency{ {0 * 8, 0 * 16}, nav_}; @@ -87,20 +84,81 @@ class WaterfallDesignerView : public View { {21 * 8, 0 * 16}}; OptionsField option_bandwidth{ - {5 * 8, 1 * 16}, + {24 * 8, 0 * 16}, 5, {}}; + MenuView menu_view{}; - 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}; + NewButton button_new{ + {0 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_file, + Theme::getInstance()->fg_blue->foreground}; - spectrum::WaterfallView waterfall{}; + 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{ + {16 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_add, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_remove_level{ + {20 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_delete, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_apply_setting{ + {screen_width - 4 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_replay, + Theme::getInstance()->fg_blue->foreground}; + + void backup_current_profile(); + void restore_current_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 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{"0,0,0,0", "86,0,0,255", "171,0,255,0", "255,255,0,0", "255,255,255,255"}; + + 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}; + + std::unique_ptr waterfall{}; MessageHandlerRegistration message_handler_freqchg{ Message::ID::FreqChangeCommand, diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 4ef180bc9..58e1e29b9 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -60,7 +60,7 @@ #include "ui_text_editor.hpp" #include "ui_touchtunes.hpp" #include "ui_weatherstation.hpp" -#include "ui_subghzd.hpp" +// #include "ui_subghzd.hpp" #include "ui_battinfo.hpp" #include "ui_external_items_menu_loader.hpp" @@ -135,7 +135,7 @@ const NavigationView::AppList NavigationView::appList = { {"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory()}, {"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, new ViewFactory()}, {"search", "Search", RX, Color::yellow(), &bitmap_icon_search, new ViewFactory()}, - {"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, new ViewFactory()}, + // {"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, new ViewFactory()}, {"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, new ViewFactory()}, /* TX ********************************************************************/ {"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory()}, From ede6106487ee4d95c093fa57c9d5c3a329e76725 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 18:35:55 +0800 Subject: [PATCH 7/9] color picker --- .../ui_waterfall_designer.cpp | 105 ++++++++++++++++++ .../ui_waterfall_designer.hpp | 77 ++++++++++++- 2 files changed, 176 insertions(+), 6 deletions(-) diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index 37666b7d2..c8e30ff45 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -51,6 +51,7 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) &button_save, &button_add_level, &button_remove_level, + &button_edit_color, &button_apply_setting, }); @@ -114,6 +115,10 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) 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(); @@ -300,6 +305,16 @@ void WaterfallDesignerView::on_remove_level() { refresh_menu_view(); } +void WaterfallDesignerView::on_edit_color() { + if (menu_view.highlighted_index() >= profile_levels.size()) return; + + auto color_picker_view = nav_.push(profile_levels[menu_view.highlighted_index()]); + color_picker_view->on_save = [this](std::string new_color) { + profile_levels[menu_view.highlighted_index()] = new_color; + refresh_menu_view(); + }; +} + void WaterfallDesignerView::backup_current_profile() { std::filesystem::path curren_wtf_path = "waterfall.txt"; std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; @@ -317,6 +332,96 @@ void WaterfallDesignerView::on_apply_setting() { } +WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str) + : nav_{nav}, + color_str_{color_str} { + add_children({&labels, + &field_red, + &field_green, + &field_blue, + &button_save}); + size_t pos = 0; + size_t next_pos = 0; + + // index + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + 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_); + + // cb + field_red.on_change = [this](int32_t) { + update_color(); + }; + + field_green.on_change = [this](int32_t) { + update_color(); + }; + + field_blue.on_change = [this](int32_t) { + update_color(); + }; + + button_save.on_select = [this](Button&) { + if (on_save) on_save(build_color_str()); + nav_.pop(); + }; + + update_color(); +} + +void WaterfallDesignerColorPickerView::focus() { + button_save.focus(); +} + +void WaterfallDesignerColorPickerView::update_color() { + 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.fill_rectangle( + {preview_rect.left() , preview_rect.top() , preview_rect.width() , preview_rect.height() }, + ui::Color(red_, green_, blue_)); +} + +void WaterfallDesignerColorPickerView::paint(Painter& painter) { + +} + +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) + + std::to_string(red_) + "," + + std::to_string(green_) + "," + + std::to_string(blue_); + } + return "0," + std::to_string(red_) + "," + std::to_string(green_) + "," + std::to_string(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 index ece60ab5e..455bd503d 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -35,6 +35,64 @@ 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 red_{0}; + uint8_t green_{0}; + uint8_t blue_{0}; + + void update_color(); + std::string build_color_str(); + Painter painter; + + Labels labels{ + {{0 * 8, 0 * 16}, "Red", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 2 * 16}, "Green", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 4 * 16}, "Blue", Theme::getInstance()->fg_light->foreground}}; + + NumberField field_red{ + {0 * 8, 1 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_green{ + {0 * 8, 3 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_blue{ + {0 * 8, 5 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + Button button_save{ + {0, 7 * 16, screen_width, 2 * 16}, + "Save"}; +}; + class WaterfallDesignerView : public View { public: WaterfallDesignerView(NavigationView& nav); @@ -109,19 +167,25 @@ class WaterfallDesignerView : public View { Theme::getInstance()->fg_blue->foreground}; NewButton button_add_level{ - {16 * 8, 8 * 16, 4 * 8, 32}, + {12 * 8, 8 * 16, 4 * 8, 32}, {}, &bitmap_icon_add, Theme::getInstance()->fg_blue->foreground}; NewButton button_remove_level{ - {20 * 8, 8 * 16, 4 * 8, 32}, + {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_paint, + Theme::getInstance()->fg_blue->foreground}; + NewButton button_apply_setting{ - {screen_width - 4 * 8, 8 * 16, 4 * 8, 32}, + {24 * 8, 8 * 16, 4 * 8, 32}, {}, &bitmap_icon_replay, Theme::getInstance()->fg_blue->foreground}; @@ -133,17 +197,18 @@ class WaterfallDesignerView : public View { 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 + 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, + 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{"0,0,0,0", "86,0,0,255", "171,0,255,0", "255,255,0,0", "255,255,255,255"}; From 5f33b7f02e99ed36fd5e973af57ba0e25243cb82 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 18:38:15 +0800 Subject: [PATCH 8/9] realtime preview --- .../external/waterfall_designer/ui_waterfall_designer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index c8e30ff45..24bd92b13 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -201,6 +201,7 @@ void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile } refresh_menu_view(); + on_apply_current_to_wtf(); } void WaterfallDesignerView::refresh_menu_view() { @@ -312,6 +313,7 @@ void WaterfallDesignerView::on_edit_color() { color_picker_view->on_save = [this](std::string new_color) { profile_levels[menu_view.highlighted_index()] = new_color; refresh_menu_view(); + on_apply_current_to_wtf(); }; } From 61378c98aaca1c795e7ed0e1d78c5f642339e531 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sun, 13 Apr 2025 20:23:23 +0800 Subject: [PATCH 9/9] FIXME: crash when add or remove entry --- .../external/waterfall_designer/main.cpp | 54 +++--- .../ui_waterfall_designer.cpp | 172 +++++++++++++++--- .../ui_waterfall_designer.hpp | 46 +++-- 3 files changed, 210 insertions(+), 62 deletions(-) diff --git a/firmware/application/external/waterfall_designer/main.cpp b/firmware/application/external/waterfall_designer/main.cpp index f21be3f3c..d78cdbd21 100644 --- a/firmware/application/external/waterfall_designer/main.cpp +++ b/firmware/application/external/waterfall_designer/main.cpp @@ -42,35 +42,35 @@ __attribute__((section(".external_app.app_waterfall_designer.application_informa /*.bitmap_data = */ { 0x00, 0x00, - 0xC0, - 0x01, - 0x80, 0x00, - 0x80, - 0x20, - 0x60, - 0x13, - 0x10, - 0x0C, - 0x88, - 0x08, - 0x84, - 0x10, - 0x84, - 0x10, - 0xC2, - 0x21, - 0x84, - 0x10, - 0x04, - 0x10, - 0x08, - 0x08, - 0x10, - 0x04, - 0x60, + 0x00, + 0xFE, + 0x7F, 0x03, - 0x80, + 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, diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index 24bd92b13..2225009ca 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -27,6 +27,8 @@ #include "file_path.hpp" #include "ui_fileman.hpp" #include "file_reader.hpp" +#include "ui_textentry.hpp" +#include "usb_serial_asyncmsg.hpp" using namespace portapack; @@ -94,9 +96,7 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) }; button_new.on_select = [this]() { - profile_levels.clear(); - current_profile_path = ""; - refresh_menu_view(); + on_create_new_profile(); }; button_open.on_select = [this]() { @@ -108,10 +108,39 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) }; button_add_level.on_select = [this]() { + portapack::async_tx_enabled = true; + UsbSerialAsyncmsg::asyncmsg("-------- pl"); + + for (auto& line : profile_levels) { + UsbSerialAsyncmsg::asyncmsg(line); + } + + UsbSerialAsyncmsg::asyncmsg("-------- pl end"); + + UsbSerialAsyncmsg::asyncmsg("-------- index"); + UsbSerialAsyncmsg::asyncmsg(to_string_dec_uint(menu_view.highlighted_index())); + + UsbSerialAsyncmsg::asyncmsg("-------- index end"); + UsbSerialAsyncmsg::asyncmsg("\n\n\n\n\n"); on_add_level(); }; button_remove_level.on_select = [this]() { + portapack::async_tx_enabled = true; + UsbSerialAsyncmsg::asyncmsg("-------- pl"); + + for (auto& line : profile_levels) { + UsbSerialAsyncmsg::asyncmsg(line); + } + + UsbSerialAsyncmsg::asyncmsg("-------- pl end"); + + UsbSerialAsyncmsg::asyncmsg("-------- index"); + UsbSerialAsyncmsg::asyncmsg(to_string_dec_uint(menu_view.highlighted_index())); + + UsbSerialAsyncmsg::asyncmsg("-------- index end"); + UsbSerialAsyncmsg::asyncmsg("\n\n\n\n\n"); + on_remove_level(); }; @@ -125,6 +154,23 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) nav_.pop(); }; + menu_view.on_highlight = [this]() { + portapack::async_tx_enabled = true; + UsbSerialAsyncmsg::asyncmsg("-------- pl"); + + for (auto& line : profile_levels) { + UsbSerialAsyncmsg::asyncmsg(line); + } + + UsbSerialAsyncmsg::asyncmsg("-------- pl end"); + + UsbSerialAsyncmsg::asyncmsg("-------- index"); + UsbSerialAsyncmsg::asyncmsg(to_string_dec_uint(menu_view.highlighted_index())); + + UsbSerialAsyncmsg::asyncmsg("-------- index end"); + UsbSerialAsyncmsg::asyncmsg("\n\n\n\n\n"); + }; + receiver_model.enable(); option_bandwidth.set_by_value(capture_rate); @@ -132,6 +178,12 @@ WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) 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(); } @@ -144,7 +196,7 @@ WaterfallDesignerView::WaterfallDesignerView( } WaterfallDesignerView::~WaterfallDesignerView() { - if(!if_apply_setting) restore_current_profile(); + if (!if_apply_setting) restore_current_profile(); receiver_model.disable(); baseband::shutdown(); } @@ -200,6 +252,12 @@ void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile } } + 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(); } @@ -253,17 +311,17 @@ void WaterfallDesignerView::refresh_menu_view() { } void WaterfallDesignerView::on_apply_current_to_wtf() { - std::filesystem::path current_path = "waterfall.txt"; - copy_file(current_profile_path, current_path); + std::filesystem::path system_read_path = "waterfall.txt"; + copy_file(current_profile_path, system_read_path); remove_child(waterfall.get()); waterfall.reset(); waterfall = std::make_unique(); add_child(waterfall.get()); - + ui::Rect waterfall_rect{0, header_height, screen_rect().width(), screen_rect().height() - header_height}; waterfall->set_parent_rect(waterfall_rect); - + set_dirty(); } @@ -297,18 +355,32 @@ void WaterfallDesignerView::on_save_profile() { } void WaterfallDesignerView::on_add_level() { - // new view to let user add + if (menu_view.highlighted_index() >= profile_levels.size()) return; + if (profile_levels[menu_view.highlighted_index()].empty()) return; + if (profile_levels[menu_view.highlighted_index()][0] == '#') return; + if (profile_levels[menu_view.highlighted_index()].find(',') == std::string::npos) return; + size_t insert_pos = menu_view.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() { - // remove entrance from vec + if (menu_view.highlighted_index() >= profile_levels.size()) return; + if (profile_levels[menu_view.highlighted_index()].empty()) return; + if (profile_levels[menu_view.highlighted_index()][0] == '#') return; + if (profile_levels[menu_view.highlighted_index()].find(',') == std::string::npos) return; profile_levels.erase(profile_levels.begin() + menu_view.highlighted_index()); refresh_menu_view(); } void WaterfallDesignerView::on_edit_color() { if (menu_view.highlighted_index() >= profile_levels.size()) return; - + if (profile_levels[menu_view.highlighted_index()].empty()) return; + if (profile_levels[menu_view.highlighted_index()][0] == '#') return; + if (profile_levels[menu_view.highlighted_index()].find(',') == std::string::npos) return; + auto color_picker_view = nav_.push(profile_levels[menu_view.highlighted_index()]); color_picker_view->on_save = [this](std::string new_color) { profile_levels[menu_view.highlighted_index()] = new_color; @@ -331,7 +403,32 @@ void WaterfallDesignerView::restore_current_profile() { } 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) @@ -341,14 +438,20 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie &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; } @@ -372,18 +475,19 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie 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(); + update_color_index(); }; field_green.on_change = [this](int32_t) { - update_color(); + update_color_index(); }; field_blue.on_change = [this](int32_t) { - update_color(); + update_color_index(); }; button_save.on_select = [this](Button&) { @@ -391,39 +495,59 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie nav_.pop(); }; - update_color(); + 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() { +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.fill_rectangle( - {preview_rect.left() , preview_rect.top() , preview_rect.width() , preview_rect.height() }, + 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) + - std::to_string(red_) + "," + - std::to_string(green_) + "," + - std::to_string(blue_); + return color_str_.substr(0, index_pos + 1) + + to_string_dec_uint(red_) + "," + + to_string_dec_uint(green_) + "," + + to_string_dec_uint(blue_); } - return "0," + std::to_string(red_) + "," + std::to_string(green_) + "," + std::to_string(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 index 455bd503d..17ec3d937 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -54,42 +54,64 @@ class WaterfallDesignerColorPickerView : public View { 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(); + void update_color_index(); std::string build_color_str(); - Painter painter; Labels labels{ - {{0 * 8, 0 * 16}, "Red", Theme::getInstance()->fg_light->foreground}, - {{0 * 8, 2 * 16}, "Green", Theme::getInstance()->fg_light->foreground}, - {{0 * 8, 4 * 16}, "Blue", Theme::getInstance()->fg_light->foreground}}; + {{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_red{ + NumberField field_index{ {0 * 8, 1 * 16}, 3, {0, 255}, 1, ' '}; - NumberField field_green{ + NumberField field_red{ {0 * 8, 3 * 16}, 3, {0, 255}, 1, ' '}; - NumberField field_blue{ + 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, 7 * 16, screen_width, 2 * 16}, + {0, screen_height - 4 * 16, screen_width, 4 * 16}, "Save"}; }; @@ -181,7 +203,7 @@ class WaterfallDesignerView : public View { NewButton button_edit_color{ {20 * 8, 8 * 16, 4 * 8, 32}, {}, - &bitmap_icon_paint, + &bitmap_icon_notepad, Theme::getInstance()->fg_blue->foreground}; NewButton button_apply_setting{ @@ -192,6 +214,7 @@ class WaterfallDesignerView : public View { 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(); @@ -211,7 +234,7 @@ class WaterfallDesignerView : public View { 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{"0,0,0,0", "86,0,0,255", "171,0,255,0", "255,255,0,0", "255,255,255,255"}; + std::vector profile_levels{}; static constexpr ui::Dim header_height = 10 * 16; @@ -224,6 +247,7 @@ class WaterfallDesignerView : public View { 3}; std::unique_ptr waterfall{}; + std::string file_name_buffer{}; // needed by text_prompy MessageHandlerRegistration message_handler_freqchg{ Message::ID::FreqChangeCommand,