diff --git a/firmware/application/apps/ui_freqman.cpp b/firmware/application/apps/ui_freqman.cpp index a48d96b0..453cda2b 100644 --- a/firmware/application/apps/ui_freqman.cpp +++ b/firmware/application/apps/ui_freqman.cpp @@ -153,28 +153,14 @@ FrequencySaveView::FrequencySaveView( add_children( {&labels, &big_display, - &button_clear, - &button_edit, &button_save, - &text_description}); + &field_description}); entry_.type = freqman_type::Single; entry_.frequency_a = value; entry_.description = to_string_timestamp(rtc_time::now()); - refresh_ui(); - button_clear.on_select = [this, &nav](Button&) { - entry_.description = ""; - refresh_ui(); - }; - - button_edit.on_select = [this, &nav](Button&) { - temp_buffer_ = entry_.description; - text_prompt(nav_, temp_buffer_, desc_edit_max, [this](std::string& new_desc) { - entry_.description = new_desc; - refresh_ui(); - }); - }; + bind(field_description, entry_.description, nav); button_save.on_select = [this, &nav](Button&) { db_.insert_entry(db_.entry_count(), entry_); @@ -182,9 +168,13 @@ FrequencySaveView::FrequencySaveView( }; } +void FrequencySaveView::focus() { + refresh_ui(); + FreqManBaseView::focus(); +} + void FrequencySaveView::refresh_ui() { big_display.set(entry_.frequency_a); - text_description.set(entry_.description); } /* FrequencyLoadView *************************************/ diff --git a/firmware/application/apps/ui_freqman.hpp b/firmware/application/apps/ui_freqman.hpp index 3ab0c75d..03a7f021 100644 --- a/firmware/application/apps/ui_freqman.hpp +++ b/firmware/application/apps/ui_freqman.hpp @@ -87,9 +87,9 @@ class FrequencySaveView : public FreqManBaseView { public: FrequencySaveView(NavigationView& nav, const rf::Frequency value); std::string title() const override { return "Save freq"; } + void focus() override; private: - std::string temp_buffer_{}; freqman_entry entry_{}; void refresh_ui(); @@ -101,15 +101,9 @@ class FrequencySaveView : public FreqManBaseView { Labels labels{ {{0 * 8, 6 * 16}, "Description:", Color::white()}}; - Text text_description{{0 * 8, 7 * 16, 30 * 8, 1 * 16}}; - - Button button_clear{ - {4 * 8, 10 * 16, 10 * 8, 2 * 16}, - "Clear"}; - - Button button_edit{ - {16 * 8, 10 * 16, 10 * 8, 2 * 16}, - "Edit"}; + TextField field_description{ + {0 * 8, 7 * 16, 30 * 8, 1 * 16}, + ""}; Button button_save{ {0 * 8, 17 * 16, 15 * 8, 2 * 16}, diff --git a/firmware/application/apps/ui_search.cpp b/firmware/application/apps/ui_search.cpp index f7b5e9b4..30bd8b8b 100644 --- a/firmware/application/apps/ui_search.cpp +++ b/firmware/application/apps/ui_search.cpp @@ -23,7 +23,9 @@ #include "ui_search.hpp" #include "baseband_api.hpp" +#include "binder.hpp" #include "string_format.hpp" +#include "ui_freqman.hpp" using namespace portapack; @@ -47,8 +49,61 @@ void RecentEntriesTable::draw( painter.draw_string(target_rect.location(), style, to_string_short_freq(entry.frequency) + " " + entry.time + " " + str_duration); } -void SearchView::focus() { - field_frequency_min.focus(); +/* SearchView ********************************************/ + +SearchView::SearchView( + NavigationView& nav) + : nav_(nav) { + baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum); + + add_children({&labels, + &field_frequency_min, + &field_frequency_max, + &field_lna, + &field_vga, + &field_threshold, + &text_mean, + &text_slices, + &text_rate, + &text_infos, + &vu_max, + &progress_timers, + &check_snap, + &options_snap, + &big_display, + &recent_entries_view}); + + baseband::set_spectrum(SEARCH_SLICE_WIDTH, 31); + + recent_entries_view.set_parent_rect({0, 28 * 8, screen_width, 12 * 8}); + recent_entries_view.on_select = [this, &nav](const SearchRecentEntry& entry) { + nav.push(entry.frequency); + }; + + text_mean.set_style(&Styles::grey); + text_slices.set_style(&Styles::grey); + text_rate.set_style(&Styles::grey); + progress_timers.set_style(&Styles::grey); + big_display.set_style(&Styles::grey); + + field_frequency_min.set_step(100'000); + bind(field_frequency_min, settings_.freq_min, nav, [this](auto) { + on_range_changed(); + }); + + field_frequency_max.set_step(100'000); + bind(field_frequency_max, settings_.freq_max, nav, [this](auto) { + on_range_changed(); + }); + + bind(field_threshold, settings_.power_threshold); + bind(check_snap, settings_.snap_search); + bind(options_snap, settings_.snap_step); + + progress_timers.set_max(DETECT_DELAY); + + on_range_changed(); + receiver_model.enable(); } SearchView::~SearchView() { @@ -56,6 +111,18 @@ SearchView::~SearchView() { baseband::shutdown(); } +void SearchView::on_show() { + baseband::spectrum_streaming_start(); +} + +void SearchView::on_hide() { + baseband::spectrum_streaming_stop(); +} + +void SearchView::focus() { + field_frequency_min.focus(); +} + void SearchView::do_detection() { uint8_t power_max = 0; int32_t bin_max = -1; @@ -83,7 +150,7 @@ void SearchView::do_detection() { if (power > overall_power_max) overall_power_max = power; - if ((power >= mean_power + power_threshold) && (power > power_max)) { + if ((power >= mean_power + settings_.power_threshold) && (power > power_max)) { power_max = power; bin_max = slices[slice].max_index; slice_max = slice; @@ -104,7 +171,7 @@ void SearchView::do_detection() { } // Check range - if ((resolved_frequency >= f_min) && (resolved_frequency <= f_max)) { + if ((resolved_frequency >= settings_.freq_min) && (resolved_frequency <= settings_.freq_max)) { duration = 0; auto& entry = ::on_packet(recent, resolved_frequency); @@ -159,16 +226,47 @@ void SearchView::do_detection() { } } -void SearchView::add_spectrum_pixel(Color color) { - // Is avoiding floats really necessary ? - bin_skip_acc += bin_skip_frac; - if (bin_skip_acc < 0x10000) - return; +void SearchView::do_timers() { + if (timing_div >= 60) { + // ~1Hz - bin_skip_acc -= 0x10000; + timing_div = 0; - if (pixel_index < 240) - spectrum_row[pixel_index++] = color; + // Update scan rate + text_rate.set(to_string_dec_uint(search_counter, 3)); + search_counter = 0; + } + + if (timing_div % 12 == 0) { + // ~5Hz + + // Update power levels + text_mean.set(to_string_dec_uint(mean_power, 3)); + + vu_max.set_value(overall_power_max); + vu_max.set_mark(mean_power + settings_.power_threshold); + } + + if (timing_div % 6 == 0) { + // ~10Hz + + // Update timing indicator + if (locked) { + progress_timers.set_max(RELEASE_DELAY); + progress_timers.set_value(RELEASE_DELAY - release_timer); + } else { + progress_timers.set_max(DETECT_DELAY); + progress_timers.set_value(detect_timer); + } + + // Increment timers + if (detect_timer < DETECT_DELAY) detect_timer++; + if (release_timer < RELEASE_DELAY) release_timer++; + + if (locked) duration++; + } + + timing_div++; } void SearchView::on_channel_spectrum(const ChannelSpectrum& spectrum) { @@ -221,22 +319,14 @@ void SearchView::on_channel_spectrum(const ChannelSpectrum& spectrum) { baseband::spectrum_streaming_start(); } -void SearchView::on_show() { - baseband::spectrum_streaming_start(); -} - -void SearchView::on_hide() { - baseband::spectrum_streaming_stop(); -} - void SearchView::on_range_changed() { - rf::Frequency slices_span, center_frequency; + rf::Frequency slices_span; + rf::Frequency center_frequency; int64_t offset; size_t slice; - f_min = field_frequency_min.value(); - f_max = field_frequency_max.value(); - search_span = abs(f_max - f_min); + // TODO: enforce min < max? + search_span = abs(settings_.freq_max - settings_.freq_min); if (search_span > SEARCH_SLICE_WIDTH) { // ex: 100M~115M (15M span): @@ -253,14 +343,14 @@ void SearchView::on_range_changed() { // offset = 0 + 2.5/2 = 1.25M offset = ((search_span - slices_span) / 2) + (SEARCH_SLICE_WIDTH / 2); // slice_start = 100M + 1.25M = 101.25M - center_frequency = std::min(f_min, f_max) + offset; + center_frequency = std::min(settings_.freq_min, settings_.freq_max) + offset; for (slice = 0; slice < slices_nb; slice++) { slices[slice].center_frequency = center_frequency; center_frequency += SEARCH_SLICE_WIDTH; } } else { - slices[0].center_frequency = (f_max + f_min) / 2; + slices[0].center_frequency = (settings_.freq_max + settings_.freq_min) / 2; receiver_model.set_target_frequency(slices[0].center_frequency); slices_nb = 1; @@ -272,139 +362,16 @@ void SearchView::on_range_changed() { slice_counter = 0; } -void SearchView::on_lna_changed(int32_t v_db) { - receiver_model.set_lna(v_db); -} +void SearchView::add_spectrum_pixel(Color color) { + // Is avoiding floats really necessary? + bin_skip_acc += bin_skip_frac; + if (bin_skip_acc < 0x10000) + return; -void SearchView::on_vga_changed(int32_t v_db) { - receiver_model.set_vga(v_db); -} + bin_skip_acc -= 0x10000; -void SearchView::do_timers() { - if (timing_div >= 60) { - // ~1Hz - - timing_div = 0; - - // Update scan rate - text_rate.set(to_string_dec_uint(search_counter, 3)); - search_counter = 0; - } - - if (timing_div % 12 == 0) { - // ~5Hz - - // Update power levels - text_mean.set(to_string_dec_uint(mean_power, 3)); - - vu_max.set_value(overall_power_max); - vu_max.set_mark(mean_power + power_threshold); - } - - if (timing_div % 6 == 0) { - // ~10Hz - - // Update timing indicator - if (locked) { - progress_timers.set_max(RELEASE_DELAY); - progress_timers.set_value(RELEASE_DELAY - release_timer); - } else { - progress_timers.set_max(DETECT_DELAY); - progress_timers.set_value(detect_timer); - } - - // Increment timers - if (detect_timer < DETECT_DELAY) detect_timer++; - if (release_timer < RELEASE_DELAY) release_timer++; - - if (locked) duration++; - } - - timing_div++; -} - -SearchView::SearchView( - NavigationView& nav) - : nav_(nav) { - baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum); - - add_children({&labels, - &field_frequency_min, - &field_frequency_max, - &field_lna, - &field_vga, - &field_threshold, - &text_mean, - &text_slices, - &text_rate, - &text_infos, - &vu_max, - &progress_timers, - &check_snap, - &options_snap, - &big_display, - &recent_entries_view}); - - baseband::set_spectrum(SEARCH_SLICE_WIDTH, 31); - - recent_entries_view.set_parent_rect({0, 28 * 8, 240, 12 * 8}); - recent_entries_view.on_select = [this, &nav](const SearchRecentEntry& entry) { - nav.push(entry.frequency); - }; - - text_mean.set_style(&Styles::grey); - text_slices.set_style(&Styles::grey); - text_rate.set_style(&Styles::grey); - progress_timers.set_style(&Styles::grey); - big_display.set_style(&Styles::grey); - - check_snap.set_value(true); - options_snap.set_selected_index(1); // 12.5kHz - - field_threshold.set_value(80); - field_threshold.on_change = [this](int32_t value) { - power_threshold = value; - }; - - field_frequency_min.set_value(receiver_model.target_frequency() - 1000000); - field_frequency_min.set_step(100000); - field_frequency_min.on_change = [this](rf::Frequency) { - this->on_range_changed(); - }; - field_frequency_min.on_edit = [this, &nav]() { - auto new_view = nav.push(receiver_model.target_frequency() - 1000000); - new_view->on_changed = [this](rf::Frequency f) { - this->field_frequency_min.set_value(f); - }; - }; - - field_frequency_max.set_value(receiver_model.target_frequency() + 1000000); - field_frequency_max.set_step(100000); - field_frequency_max.on_change = [this](rf::Frequency) { - this->on_range_changed(); - }; - field_frequency_max.on_edit = [this, &nav]() { - auto new_view = nav.push(receiver_model.target_frequency() + 1000000); - new_view->on_changed = [this](rf::Frequency f) { - this->field_frequency_max.set_value(f); - }; - }; - - field_lna.set_value(receiver_model.lna()); - field_lna.on_change = [this](int32_t v) { - this->on_lna_changed(v); - }; - - field_vga.set_value(receiver_model.vga()); - field_vga.on_change = [this](int32_t v_db) { - this->on_vga_changed(v_db); - }; - - progress_timers.set_max(DETECT_DELAY); - - on_range_changed(); - - receiver_model.enable(); + if (pixel_index < screen_width) + spectrum_row[pixel_index++] = color; } } /* namespace ui */ diff --git a/firmware/application/apps/ui_search.hpp b/firmware/application/apps/ui_search.hpp index 1169a49b..f4551632 100644 --- a/firmware/application/apps/ui_search.hpp +++ b/firmware/application/apps/ui_search.hpp @@ -20,14 +20,13 @@ * Boston, MA 02110-1301, USA. */ +#include "app_settings.hpp" #include "receiver_model.hpp" +#include "recent_entries.hpp" #include "radio_state.hpp" - #include "spectrum_color_lut.hpp" - #include "ui_receiver.hpp" #include "ui_styles.hpp" -#include "recent_entries.hpp" namespace ui { @@ -95,6 +94,26 @@ class SearchView : public View { SEARCH_SLICE_WIDTH /* sampling rate */, ReceiverModel::Mode::SpectrumAnalysis}; + // Settings + struct SearchSettings { + uint32_t power_threshold = 80; + rf::Frequency freq_min = 100'000'000; + rf::Frequency freq_max = 400'000'000; + bool snap_search = true; + uint32_t snap_step = 12'500; + }; + SearchSettings settings_{}; + app_settings::SettingsManager app_settings_{ + "rx_search"sv, + app_settings::Mode::RX, + { + {"power_threshold"sv, &settings_.power_threshold}, + {"freq_min"sv, &settings_.freq_min}, + {"freq_max"sv, &settings_.freq_max}, + {"snap_search"sv, &settings_.snap_search}, + {"snap_step"sv, &settings_.snap_step}, + }}; + struct slice_t { rf::Frequency center_frequency; uint8_t max_power; @@ -103,33 +122,36 @@ class SearchView : public View { int16_t index; } slices[32]; - uint32_t bin_skip_acc{0}, bin_skip_frac{}; - uint32_t pixel_index{0}; - std::array spectrum_row = {0}; - ChannelSpectrumFIFO* fifo{nullptr}; - rf::Frequency f_min{0}, f_max{0}; - uint8_t detect_timer{0}, release_timer{0}, timing_div{0}; - uint8_t overall_power_max{0}; - uint32_t mean_power{0}, mean_acc{0}; - uint32_t duration{0}; - uint32_t power_threshold{80}; // Todo: Put this in persistent / settings - rf::Frequency slice_start{0}; - uint8_t slices_nb{0}; - uint8_t slice_counter{0}; - int16_t last_bin{0}; - uint32_t last_slice{0}; - Coord last_tick_pos{0}; - rf::Frequency search_span{0}, resolved_frequency{0}; - uint16_t locked_bin{0}; - uint8_t search_counter{0}; - bool locked{false}; + uint32_t bin_skip_acc = 0; + uint32_t bin_skip_frac = 0; + uint32_t pixel_index = 0; + std::array spectrum_row{}; + ChannelSpectrumFIFO* fifo = nullptr; + uint8_t detect_timer = 0; + uint8_t release_timer = 0; + uint8_t timing_div = 0; + uint8_t overall_power_max{0}; + uint32_t mean_power = 0; + uint32_t mean_acc = 0; + uint32_t duration = 0; + + rf::Frequency slice_start = 0; + uint8_t slices_nb = 0; + uint8_t slice_counter = 0; + int16_t last_bin = 0; + uint32_t last_slice = 0; + Coord last_tick_pos = 0; + rf::Frequency search_span = 0; + rf::Frequency resolved_frequency = 0; + uint16_t locked_bin = 0; + uint8_t search_counter = 0; + bool locked = false; + + void do_detection(); + void do_timers(); void on_channel_spectrum(const ChannelSpectrum& spectrum); void on_range_changed(); - void do_detection(); - void on_lna_changed(int32_t v_db); - void on_vga_changed(int32_t v_db); - void do_timers(); void add_spectrum_pixel(Color color); const RecentEntriesColumns columns{{{"Frequency", 9}, @@ -150,6 +172,7 @@ class SearchView : public View { {1 * 8, 1 * 16}}; FrequencyField field_frequency_max{ {11 * 8, 1 * 16}}; + LNAGainField field_lna{ {22 * 8, 1 * 16}}; VGAGainField field_vga{ @@ -191,10 +214,10 @@ class SearchView : public View { {17 * 8, 15 * 8}, // Position 7, // Length { // Options - {"25kHz ", 25000}, - {"12.5kHz", 12500}, - {"8.33kHz", 8333}, - {"2.5kHz", 2500}, + {"25kHz ", 25'000}, + {"12.5kHz", 12'500}, + {"8.33kHz", 8'333}, + {"2.5kHz", 2'500}, {"500Hz", 500}}}; BigFrequency big_display{ @@ -207,6 +230,7 @@ class SearchView : public View { const auto message = *reinterpret_cast(p); this->fifo = message.fifo; }}; + MessageHandlerRegistration message_handler_frame_sync{ Message::ID::DisplayFrameSync, [this](const Message* const) { diff --git a/firmware/application/binder.hpp b/firmware/application/binder.hpp index eabbd03b..1b4c01ec 100644 --- a/firmware/application/binder.hpp +++ b/firmware/application/binder.hpp @@ -52,6 +52,15 @@ struct NoOp { * adequate lifetime of the referenced instances. */ +template +void bind(Checkbox& check, T& value, Fn fn = Fn{}) { + check.set_value(value); + check.on_select = [&value, fn](Checkbox&, bool b) { + value = b; + fn(value); + }; +} + template void bind(NumberField& field, T& value, Fn fn = Fn{}) { field.set_value(value); diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 6a2e7c15..31b8eac1 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -28,6 +28,7 @@ #include #include +#include "irq_controls.hpp" #include "string_format.hpp" using namespace portapack; @@ -1730,9 +1731,15 @@ bool TextEdit::on_key(const KeyEvent key) { cursor_pos_--; else if (key == KeyEvent::Right && cursor_pos_ < text_.length()) cursor_pos_++; - else if (key == KeyEvent::Select) - insert_mode_ = !insert_mode_; - else + else if (key == KeyEvent::Select) { + if (key_is_long_pressed(key)) { + // Delete text to the cursor. + text_ = text_.substr(cursor_pos_); + set_cursor(0); + } else { + insert_mode_ = !insert_mode_; + } + } else return false; set_dirty(); @@ -1760,6 +1767,19 @@ bool TextEdit::on_touch(const TouchEvent event) { return true; } +void TextEdit::on_focus() { + // Enable long press on "Select". + SwitchesState config; + config[toUType(Switch::Sel)] = true; + set_switches_long_press_config(config); +} + +void TextEdit::on_blur() { + // Reset long press. + SwitchesState config{}; + set_switches_long_press_config(config); +} + /* TextField *************************************************************/ TextField::TextField(Rect parent_rect, std::string text) diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index fcf24ca7..57dab0a8 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -689,6 +689,9 @@ class TextEdit : public Widget { bool on_encoder(const EncoderEvent delta) override; bool on_touch(const TouchEvent event) override; + void on_focus() override; + void on_blur() override; + protected: std::string& text_; size_t max_length_;