diff --git a/firmware/application/apps/ui_looking_glass_app.cpp b/firmware/application/apps/ui_looking_glass_app.cpp index 47aa72be..17fdcca9 100644 --- a/firmware/application/apps/ui_looking_glass_app.cpp +++ b/firmware/application/apps/ui_looking_glass_app.cpp @@ -27,7 +27,6 @@ using namespace portapack; namespace ui { - void GlassView::focus() { field_marker.focus(); @@ -40,6 +39,19 @@ namespace ui baseband::shutdown(); } + void GlassView::adjust_range(int64_t* f_min, int64_t* f_max, int64_t width) { + int64_t span = *f_max - *f_min; + int64_t num_intervals = span / width; + if( span % width != 0 ) + { + num_intervals++; + } + int64_t new_span = num_intervals * width; + int64_t delta_span = (new_span - span) / 2; + *f_min -= delta_span; + *f_max += delta_span; + } + void GlassView::on_lna_changed(int32_t v_db) { receiver_model.set_lna(v_db); @@ -64,12 +76,12 @@ namespace ui } } - void GlassView::add_spectrum_pixel(int16_t color) + void GlassView::add_spectrum_pixel( uint8_t power ) { - static uint64_t last_max_freq = 0 ; + static int64_t last_max_freq = 0 ; - spectrum_row[pixel_index] = spectrum_rgb3_lut[color] ; - spectrum_data[pixel_index] = ( live_frequency_integrate * spectrum_data[pixel_index] + color ) / (live_frequency_integrate + 1); // smoothing + spectrum_row[pixel_index] = spectrum_rgb3_lut[power] ; // row of colors + spectrum_data[pixel_index] = ( live_frequency_integrate * spectrum_data[pixel_index] + power ) / (live_frequency_integrate + 1); // smoothing pixel_index ++ ; if (pixel_index == 240) // got an entire waterfall line @@ -107,7 +119,7 @@ namespace ui if( last_max_freq != max_freq_hold ) { last_max_freq = max_freq_hold ; - freq_stats.set( "MAX:"+to_string_short_freq( max_freq_hold ) ); + freq_stats.set( "MAX HOLD: "+to_string_short_freq( max_freq_hold ) ); } PlotMarker(field_marker.value()); } @@ -124,51 +136,84 @@ namespace ui void GlassView::on_channel_spectrum(const ChannelSpectrum &spectrum) { baseband::spectrum_streaming_stop(); - - // Convert bins of this spectrum slice into a representative max_power and when enough, into pixels - // Spectrum.db has 256 bins. Center 12 bins are ignored (DC spike is blanked) Leftmost and rightmost 2 bins are ignored - // All things said and done, we actually need 240 of those bins: - for (uint8_t bin = 0; bin < 240; bin++) + if( fast_scan ) { - if (bin < 120) + // Convert bins of this spectrum slice into a representative max_power and when enough, into pixels + // Spectrum.db has 256 bins. Center 12 bins are ignored (DC spike is blanked) Leftmost and rightmost 2 bins are ignored + // All things said and done, we actually need 240 of those bins: + for (uint8_t bin = 0; bin < 240; bin++) + { + if (bin < 120) + { + if (spectrum.db[134 + bin] > max_power) // 134 + max_power = spectrum.db[134 + bin]; + } + else + { + if (spectrum.db[bin - 118] > max_power) // 118 + max_power = spectrum.db[bin - 118]; + } + + bins_Hz_size += each_bin_size; // add this bin Hz count into the "pixel fulfilled bag of Hz" + + if (bins_Hz_size >= marker_pixel_step) // new pixel fullfilled + { + if (min_color_power < max_power) + add_spectrum_pixel(max_power); // Pixel will represent max_power + else + add_spectrum_pixel(0); // Filtered out, show black + + max_power = 0; + + if (!pixel_index) // Received indication that a waterfall line has been completed + { + bins_Hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin" + f_center = f_center_ini; // Start a new sweep + radio::set_tuning_frequency(f_center); // tune rx for this new slice directly, faster than using persistent memory saving + chThdSleepMilliseconds(10); + baseband::spectrum_streaming_start(); // Do the RX + return; + } + bins_Hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel + } + } + + f_center += LOOKING_GLASS_SLICE_WIDTH; // Move into the next bandwidth slice NOTE: spectrum.sampling_rate = LOOKING_GLASS_SLICE_WIDTH + } + else //slow scan + { + for( int16_t bin = 0 ; bin < 120 ; bin++) { if (spectrum.db[134 + bin] > max_power) // 134 - max_power = spectrum.db[134 + bin]; - } - else - { - if (spectrum.db[bin - 118] > max_power) // 118 - max_power = spectrum.db[bin - 118]; - } + max_power = spectrum.db[134 + bin]; - bins_Hz_size += each_bin_size; // add this bin Hz count into the "pixel fulfilled bag of Hz" + bins_Hz_size += each_bin_size; // add this bin Hz count into the "pixel fulfilled bag of Hz" - if (bins_Hz_size >= marker_pixel_step) // new pixel fullfilled - { - if (min_color_power < max_power) - add_spectrum_pixel(max_power); // Pixel will represent max_power - else - add_spectrum_pixel(0); // Filtered out, show black - - max_power = 0; - - if (!pixel_index) // Received indication that a waterfall line has been completed + if (bins_Hz_size >= marker_pixel_step) // new pixel fullfilled { - bins_Hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin" - f_center = f_center_ini; // Start a new sweep - radio::set_tuning_frequency(f_center); // tune rx for this new slice directly, faster than using persistent memory saving - chThdSleepMilliseconds(10); - baseband::spectrum_streaming_start(); // Do the RX - return; - } - bins_Hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel - } - } + if (min_color_power < max_power) + add_spectrum_pixel(max_power); // Pixel will represent max_power + else + add_spectrum_pixel(0); // Filtered out, show black - f_center += LOOKING_GLASS_SLICE_WIDTH; // Move into the next bandwidth slice NOTE: spectrum.sampling_rate = LOOKING_GLASS_SLICE_WIDTH + max_power = 0; + + if (!pixel_index) // Received indication that a waterfall line has been completed + { + bins_Hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin" + f_center = f_center_ini; // Start a new sweep + radio::set_tuning_frequency(f_center); // tune rx for this new slice directly, faster than using persistent memory saving + chThdSleepMilliseconds(10); + baseband::spectrum_streaming_start(); // Do the RX + return; + } + bins_Hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel + } + } + f_center += LOOKING_GLASS_SLICE_WIDTH / 2 ; + } radio::set_tuning_frequency(f_center); // tune rx for this new slice directly, faster than using persistent memory saving chThdSleepMilliseconds(5); - // receiver_model.set_tuning_frequency(f_center); //tune rx for this slice baseband::spectrum_streaming_start(); // Do the RX } @@ -194,13 +239,20 @@ namespace ui field_marker.set_range(f_min, f_max); // Move the marker between range field_marker.set_value(f_min + (search_span / 2)); // Put MARKER AT MIDDLE RANGE - text_range.set(to_string_dec_uint(search_span)); + if( locked_range ) + { + button_range.set_text(">"+to_string_dec_uint(search_span)+"<"); + } + else + { + button_range.set_text(" "+to_string_dec_uint(search_span)+" "); + } f_min = (f_min)*MHZ_DIV; // Transpose into full frequency realm f_max = (f_max)*MHZ_DIV; - search_span = search_span * MHZ_DIV; + adjust_range( &f_min , &f_max , 240 ); - marker_pixel_step = search_span / 240; // Each pixel value in Hz + marker_pixel_step = (f_max - f_min) / 240; // Each pixel value in Hz text_marker_pm.set(to_string_dec_uint((marker_pixel_step / X2_MHZ_DIV) + 1)); // Give idea of +/- marker precision int32_t marker_step = marker_pixel_step / MHZ_DIV; @@ -249,8 +301,9 @@ namespace ui &field_frequency_max, &field_lna, &field_vga, - &text_range, + &button_range, &steps_config, + &scan_type, &view_config, &level_integration, &filter_config, @@ -265,89 +318,100 @@ namespace ui load_Presets(); // Load available presets from TXT files (or default) - field_frequency_min.set_value(presets_db[0].min); // Defaults to first preset - field_frequency_min.set_step( steps ); field_frequency_min.on_change = [this](int32_t v) { reset_live_view( true ); - int32_t steps_ = steps ; - if( steps_ < 24 ) - steps_ = 24 ; - if( v > 7200 - steps_ ) + int32_t min_size = steps ; + if( locked_range ) + min_size = search_span ; + if( min_size < 20 ) + min_size = 20 ; + if( v > 7200 - min_size ) { - v = 7200 - steps_ ; + v = 7200 - min_size ; field_frequency_min.set_value( v ); } - if (v >= (field_frequency_max.value() - steps_ ) ) - field_frequency_max.set_value( v + steps_ ); + if (v > (field_frequency_max.value() - min_size ) ) + field_frequency_max.set_value( v + min_size ); + if( locked_range ) + field_frequency_max.set_value( v + min_size ); this->on_range_changed(); }; + field_frequency_min.set_value(presets_db[0].min); // Defaults to first preset + field_frequency_min.set_step( steps ); field_frequency_min.on_select = [this, &nav](NumberField& field) { auto new_view = nav_.push(field_frequency_min.value()*1000000); new_view->on_changed = [this, &field](rf::Frequency f) { int32_t freq = f / 1000000 ; - int32_t steps_ = steps ; - if( steps_ < 24 ) - steps_ = 24 ; - if( freq > (7200 - steps_ ) ) - freq= 7200 - steps_ ; + int32_t min_size = steps ; + if( locked_range ) + min_size = search_span ; + if( min_size < 20 ) + min_size = 20 ; + if( freq > (7200 - min_size ) ) + freq = 7200 - min_size ; field_frequency_min.set_value( freq ); - if( field_frequency_max.value() < ( freq + steps_ ) ) - field_frequency_max.set_value( freq + steps_ ); + if( field_frequency_max.value() < ( freq + min_size ) ) + field_frequency_max.set_value( freq + min_size ); this->on_range_changed(); }; }; - field_frequency_max.set_value(presets_db[0].max); // Defaults to first preset - field_frequency_max.set_step( steps ); field_frequency_max.on_change = [this](int32_t v) { reset_live_view( true ); - int32_t steps_ = steps ; - if( steps_ < 24 ) - steps_ = 24 ; - if( v < steps_ ) + int32_t min_size = steps ; + if( locked_range ) + min_size = search_span ; + if( min_size < 20 ) + min_size = 20 ; + if( v < min_size ) { - v = steps_ ; + v = min_size ; field_frequency_max.set_value( v ); } - if (v < (field_frequency_min.value() + steps_) ) - field_frequency_min.set_value(v - steps_); + if (v < (field_frequency_min.value() + min_size) ) + field_frequency_min.set_value(v - min_size); + if( locked_range ) + field_frequency_min.set_value( v - min_size ); this->on_range_changed(); }; + field_frequency_max.set_value(presets_db[0].max); // Defaults to first preset + field_frequency_max.set_step( steps ); field_frequency_max.on_select = [this, &nav](NumberField& field) { auto new_view = nav_.push(field_frequency_max.value()*1000000); new_view->on_changed = [this, &field](rf::Frequency f) { - int32_t steps_ = steps ; - if( steps_ < 24 ) - steps_ = 24 ; + int32_t min_size = steps ; + if( locked_range ) + min_size = search_span ; + if( min_size < 20 ) + min_size = 20 ; int32_t freq = f / 1000000 ; - if( freq < 24 ) - freq = 24 ; + if( freq < min_size ) + freq = min_size ; field_frequency_max.set_value( freq ); - if( field_frequency_min.value() > ( freq - steps) ) - field_frequency_min.set_value( freq - steps ); + if( field_frequency_min.value() > ( freq - min_size) ) + field_frequency_min.set_value( freq - min_size ); this->on_range_changed(); }; }; - field_lna.set_value(receiver_model.lna()); field_lna.on_change = [this](int32_t v) { reset_live_view( true ); this->on_lna_changed(v); }; + field_lna.set_value(receiver_model.lna()); - field_vga.set_value(receiver_model.vga()); field_vga.on_change = [this](int32_t v_db) { reset_live_view( true ); this->on_vga_changed(v_db); }; + field_vga.set_value(receiver_model.vga()); - steps_config.set_selected_index(3); //default of 250 Mhz steps steps_config.on_change = [this](size_t n, OptionsField::value_t v) { (void)n; @@ -355,6 +419,14 @@ namespace ui field_frequency_max.set_step( v ); steps = v ; }; + steps_config.set_selected_index(0); //default of 1 Mhz steps + + scan_type.on_change = [this](size_t n, OptionsField::value_t v) + { + (void)n; + fast_scan = v ; + }; + scan_type.set_selected_index(0); // default legacy fast scan view_config.on_change = [this](size_t n, OptionsField::value_t v) { @@ -400,14 +472,14 @@ namespace ui reset_live_view( true ); live_frequency_integrate = v ; }; - level_integration.set_selected_index(2); //default integration of ( 3 * old value + new_value ) / 4 + level_integration.set_selected_index(3); //default integration of ( 3 * old value + new_value ) / 4 - filter_config.set_selected_index(0); filter_config.on_change = [this](size_t n, OptionsField::value_t v) { (void)n; reset_live_view( true ); min_color_power = v; }; + filter_config.set_selected_index(0); range_presets.on_change = [this](size_t n, OptionsField::value_t v) { @@ -432,11 +504,26 @@ namespace ui nav_.push(); // Jump into audio view }; - field_trigger.set_value(32); // Defaults to 32, as normal triggering resolution field_trigger.on_change = [this](int32_t v) { baseband::set_spectrum(LOOKING_GLASS_SLICE_WIDTH, v); }; + field_trigger.set_value(32); // Defaults to 32, as normal triggering resolution + + button_range.on_select = [this](Button&) { + if( locked_range ) + { + locked_range = false ; + button_range.set_style(&style_white); + button_range.set_text(" "+to_string_dec_uint(search_span)+" "); + } + else + { + locked_range = true ; + button_range.set_style(&style_red); + button_range.set_text(">"+to_string_dec_uint(search_span)+"<"); + } + }; button_jump.on_select = [this](Button&) { receiver_model.set_tuning_frequency(max_freq_hold); // Center tune rx in marker freq. diff --git a/firmware/application/apps/ui_looking_glass_app.hpp b/firmware/application/apps/ui_looking_glass_app.hpp index 83908de7..6a885c44 100644 --- a/firmware/application/apps/ui_looking_glass_app.hpp +++ b/firmware/application/apps/ui_looking_glass_app.hpp @@ -37,15 +37,13 @@ namespace ui { -#define LOOKING_GLASS_SLICE_WIDTH 19999920 // Each slice bandwidth 20 MHz and a multiple of 240 - // since we are using LOOKING_GLASS_SLICE_WIDTH/240 as the each_bin_size - // it should also be a multiple of 2 since we are using LOOKING_GLASS_SLICE_WIDTH / 2 as centering freq #define MHZ_DIV 1000000 #define X2_MHZ_DIV 2000000 class GlassView : public View { public: + GlassView(NavigationView &nav); GlassView( const GlassView &); @@ -67,16 +65,33 @@ namespace ui rf::Frequency max{}; std::string label{}; }; + + const Style style_white { // free range + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::white(), + }; + + const Style style_red { // locked range + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::red(), + }; std::vector presets_db{}; + int64_t LOOKING_GLASS_SLICE_WIDTH = 19999920; // Each slice bandwidth 20 MHz and a multiple of 240 + // since we are using LOOKING_GLASS_SLICE_WIDTH/240 as the each_bin_size + // it should also be a multiple of 2 since we are using LOOKING_GLASS_SLICE_WIDTH / 2 as centering freq + + void adjust_range(int64_t* f_min, int64_t* f_max, int64_t width); void on_channel_spectrum(const ChannelSpectrum& spectrum); void do_timers(); void on_range_changed(); void on_lna_changed(int32_t v_db); void on_vga_changed(int32_t v_db); void reset_live_view( bool clear_screen ); - void add_spectrum_pixel(int16_t color); + void add_spectrum_pixel(uint8_t power); void PlotMarker(rf::Frequency pos); void load_Presets(); void txtline_process(std::string& line); @@ -96,15 +111,17 @@ namespace ui std::array spectrum_data = { 0 }; ChannelSpectrumFIFO* fifo { nullptr }; uint8_t max_power = 0; - int32_t steps = 250 ; // default of 250 Mhz steps + int32_t steps = 0 ; uint8_t live_frequency_view = 0 ; int16_t live_frequency_integrate = 3 ; - uint64_t max_freq_hold = 0 ; + int64_t max_freq_hold = 0 ; int16_t max_freq_power = -1000 ; + bool fast_scan = true ; // default to legacy fast scan + bool locked_range = false ; Labels labels{ {{0, 0}, "MIN: MAX: LNA VGA ", Color::light_grey()}, - {{0, 1 * 16}, " RANGE: FILTER: AMP:", Color::light_grey()}, + {{0, 1 * 16}, "RANGE: FILTER: AMP:", Color::light_grey()}, {{0, 2 * 16}, "PRESET:", Color::light_grey()}, {{0, 3 * 16}, "MARKER: MHz +/- MHz", Color::light_grey()}, {{0, 4 * 16}, "RES: STEP:", Color::light_grey()} @@ -134,12 +151,12 @@ namespace ui { 27 * 8, 0 * 16 } }; - Text text_range{ + Button button_range{ {7 * 8, 1 * 16, 4 * 8, 16}, ""}; OptionsField filter_config{ - {19 * 8, 1 * 16}, + {20 * 8, 1 * 16}, 4, { {"OFF ", 0}, @@ -176,38 +193,51 @@ namespace ui ' '}; OptionsField steps_config{ - { 14 * 8, 4 * 16}, - 4, - { - {"1", 1}, - {"50", 50}, - {"100", 100}, - {"250", 250}, - {"500", 500}, - }}; + { 13 * 8, 4 * 16}, + 3, + { + {"1", 1}, + {"25", 25}, + {"50", 50}, + {"100", 100}, + {"250", 250}, + {"500", 500}, + } + }; - OptionsField view_config{ + OptionsField scan_type{ + { 17 * 8, 4 * 16}, + 2, + { + {"F-", true }, + {"S-", false }, + } + }; + + OptionsField view_config{ { 19 * 8, 4 * 16}, - 7, - { - {"SPCTR-V", 0 }, - {"LEVEL-V", 1 }, - {"PEAK-V" , 2 }, - }}; + 7, + { + {"SPCTR-V", 0 }, + {"LEVEL-V", 1 }, + {"PEAK-V" , 2 }, + } + }; OptionsField level_integration{ { 27 * 8, 4 * 16}, - 2, - { - {"x1", 1 }, - {"x2", 2 }, - {"x3", 3 }, - {"x4", 4 }, - {"x5", 5 }, - {"x6", 6 }, - {"x7", 7 }, - {"x8", 8 }, - {"x9", 9 }, + 2, + { + {"x0", 0 }, + {"x1", 1 }, + {"x2", 2 }, + {"x3", 3 }, + {"x4", 4 }, + {"x5", 5 }, + {"x6", 6 }, + {"x7", 7 }, + {"x8", 8 }, + {"x9", 9 }, }}; Button button_jump {