diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 5cdd3c65f..27b2dd476 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -274,6 +274,7 @@ set(CPPSRC apps/soundboard_app.cpp apps/ui_recon.cpp apps/ui_recon_settings.cpp + apps/ui_level.cpp apps/tpms_app.cpp apps/tpms_app.cpp protocols/aprs.cpp diff --git a/firmware/application/apps/ui_level.cpp b/firmware/application/apps/ui_level.cpp new file mode 100644 index 000000000..2c5e2a424 --- /dev/null +++ b/firmware/application/apps/ui_level.cpp @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2015 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_level.hpp" +#include "ui_fileman.hpp" +#include "file.hpp" + +using namespace portapack; +using portapack::memory::map::backup_ram; + +namespace ui { + + bool LevelView::check_sd_card() + { + return (sd_card::status() == sd_card::Status::Mounted) ? true : false; + } + + void LevelView::focus() { + button_frequency.focus(); + } + + LevelView::~LevelView() { + + // save app settings + app_settings.lna = field_lna.value(); + app_settings.vga = field_vga.value(); + app_settings.rx_amp = field_rf_amp.value(); + + settings.save("level", &app_settings); + + receiver_model.disable(); + baseband::shutdown(); + } + + LevelView::LevelView( NavigationView& nav) : nav_ { nav } { + + add_children( { + &labels, + &field_lna, + &field_vga, + &field_rf_amp, + &field_volume, + &field_bw, + &field_mode, + &step_mode, + &button_frequency, + &text_ctcss, + &freq_stats_rssi, + &freq_stats_db, + &audio_mode, + &peak_mode, + &rssi, + &rssi_graph + } ); + + rssi.set_vertical_rssi( true ); + + field_volume.on_change = [this](int32_t v) { this->on_headphone_volume_changed(v); }; + field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99); + + // Level directory + if( check_sd_card() ) { // Check to see if SD Card is mounted + make_new_directory( u"/LEVEL" ); + sd_card_mounted = true ; + } + + change_mode(NFM_MODULATION); //Start on AM + field_mode.set_by_value(NFM_MODULATION); //Reflect the mode into the manual selector + + //HELPER: Pre-setting a manual range, based on stored frequency + freq = persistent_memory::tuned_frequency(); + receiver_model.set_tuning_frequency( freq ); + button_frequency.set_text( "<" + to_string_short_freq( freq ) + " MHz>" ); + + // load auto common app settings + if( sd_card_mounted ) + { + auto rc = settings.load("level", &app_settings); + if(rc == SETTINGS_OK) { + field_lna.set_value(app_settings.lna); + field_vga.set_value(app_settings.vga); + field_rf_amp.set_value(app_settings.rx_amp); + receiver_model.set_rf_amp(app_settings.rx_amp); + } + } + + button_frequency.on_select = [this, &nav](ButtonWithEncoder& button) { + auto new_view = nav_.push(freq); + new_view->on_changed = [this, &button](rf::Frequency f) { + freq = f ; + receiver_model.set_tuning_frequency( f ); // Retune to actual freq + button_frequency.set_text( "<" + to_string_short_freq( freq ) + " MHz>" ); + }; + }; + + button_frequency.on_change = [this]() { + int64_t def_step = freqman_entry_get_step_value( step_mode.selected_index() ); + freq = freq + ( button_frequency.get_encoder_delta() * def_step ); + if( freq < 1 ) + { + freq = 1 ; + } + if( freq > ( MAX_UFREQ - def_step ) ) + { + freq = MAX_UFREQ ; + } + button_frequency.set_encoder_delta( 0 ); + + receiver_model.set_tuning_frequency( freq ); // Retune to actual freq + button_frequency.set_text( "<" + to_string_short_freq( freq ) + " MHz>" ); + }; + + field_mode.on_change = [this](size_t, OptionsField::value_t v) { + if( v != -1 ) + { + receiver_model.disable(); + baseband::shutdown(); + change_mode(v); + if( audio_mode.selected_index() != 0 ) + { + audio::output::start(); + } + receiver_model.enable(); + } + }; + + audio_mode.on_change = [this](size_t, OptionsField::value_t v) { + if( v == 0 ) + { + audio::output::stop(); + } + else if( v == 1 ) + { + audio::output::start(); + this->on_headphone_volume_changed( (receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99 ); + } + else + { + + } + }; + + + peak_mode.on_change = [this](size_t, OptionsField::value_t v) { + if( v == 0 ) + { + rssi.set_peak( false , 0 ); + } + else + { + rssi.set_peak( true , v ); + } + }; + + // default peak value + peak_mode.set_selected_index(1); + //FILL STEP OPTIONS + freqman_set_modulation_option( field_mode ); + freqman_set_step_option_short( step_mode ); + freq_stats_rssi.set_style(&style_white); + freq_stats_db.set_style(&style_white); + } + + void LevelView::on_headphone_volume_changed(int32_t v) { + const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max; + receiver_model.set_headphone_volume(new_volume); + } + + + void LevelView::on_statistics_update(const ChannelStatistics& statistics) { + static int last_max_db = -1000 ; + static int last_min_rssi = -1000 ; + static int last_avg_rssi = -1000 ; + static int last_max_rssi = -1000 ; + + rssi_graph.add_values( rssi.get_min() , rssi.get_avg() , rssi.get_max() , statistics.max_db ); + + bool refresh_db = false ; + bool refresh_rssi = false ; + + if( last_max_db != statistics.max_db ) + { + refresh_db = true ; + } + if( last_min_rssi != rssi.get_min() || last_avg_rssi != rssi.get_avg() || last_max_rssi != rssi.get_max() ) + { + refresh_rssi = true ; + } + if( refresh_db ) + { + last_max_db = statistics.max_db ; + freq_stats_db.set( "Power: "+to_string_dec_int( statistics.max_db )+" db" ); + } + if( refresh_rssi ) + { + last_min_rssi = rssi.get_min(); + last_avg_rssi = rssi.get_avg(); + last_max_rssi = rssi.get_max(); + freq_stats_rssi.set( "RSSI: "+to_string_dec_int( rssi.get_min() )+"/"+to_string_dec_int( rssi.get_avg() )+"/"+to_string_dec_int( rssi.get_max() )+" db" ); + } + } /* on_statistic_updates */ + + size_t LevelView::change_mode( freqman_index_t new_mod ) { + + field_bw.on_change = [this](size_t n, OptionsField::value_t) { (void)n; }; + + switch( new_mod ) { + case AM_MODULATION: + freqman_set_bandwidth_option( new_mod , field_bw ); + //bw DSB (0) default + field_bw.set_selected_index(0); + baseband::run_image(portapack::spi_flash::image_tag_am_audio); + receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); + receiver_model.set_am_configuration(field_bw.selected_index()); + field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_am_configuration(n); }; + receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); + text_ctcss.set(" "); + break; + case NFM_MODULATION: + freqman_set_bandwidth_option( new_mod , field_bw ); + //bw 16k (2) default + field_bw.set_selected_index(2); + baseband::run_image(portapack::spi_flash::image_tag_nfm_audio); + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + receiver_model.set_nbfm_configuration(field_bw.selected_index()); + field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_nbfm_configuration(n); }; + receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); + break; + case WFM_MODULATION: + freqman_set_bandwidth_option( new_mod , field_bw ); + //bw 200k (0) only/default + field_bw.set_selected_index(0); + baseband::run_image(portapack::spi_flash::image_tag_wfm_audio); + receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio); + receiver_model.set_wfm_configuration(field_bw.selected_index()); + field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_wfm_configuration(n); }; + receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); + text_ctcss.set(" "); + break; + default: + break; + } + return step_mode.selected_index(); + } + + + void LevelView::handle_coded_squelch(const uint32_t value) { + static int32_t last_idx = -1 ; + + float diff, min_diff = value; + size_t min_idx { 0 }; + size_t c; + + if( field_mode.selected_index() != NFM_MODULATION ) + { + text_ctcss.set(" "); + return ; + } + + // Find nearest match + for (c = 0; c < tone_keys.size(); c++) { + diff = abs(((float)value / 100.0) - tone_keys[c].second); + if (diff < min_diff) { + min_idx = c; + min_diff = diff; + } + } + + // Arbitrary confidence threshold + if( last_idx < 0 || (unsigned)last_idx != min_idx ) + { + last_idx = min_idx ; + if (min_diff < 40) + text_ctcss.set(tone_keys[min_idx].first); + else + text_ctcss.set(" "); + } + } + +} /* namespace ui */ diff --git a/firmware/application/apps/ui_level.hpp b/firmware/application/apps/ui_level.hpp new file mode 100644 index 000000000..25473e4db --- /dev/null +++ b/firmware/application/apps/ui_level.hpp @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2015 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 _UI_LEVEL +#define _UI_LEVEL + +#include "ui.hpp" +#include "receiver_model.hpp" +#include "ui_receiver.hpp" +#include "ui_font_fixed_8x16.hpp" +#include "freqman.hpp" +#include "analog_audio_app.hpp" +#include "audio.hpp" +#include "ui_mictx.hpp" +#include "portapack_persistent_memory.hpp" +#include "baseband_api.hpp" +#include "ui_spectrum.hpp" +#include "string_format.hpp" +#include "file.hpp" +#include "app_settings.hpp" + + +namespace ui { + + class LevelView : public View { + public: + LevelView(NavigationView& nav); + ~LevelView(); + + void focus() override; + + void big_display_freq( int64_t f ); + + void on_audio_spectrum(const AudioSpectrum* spectrum); + + const Style style_grey { // level + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::grey(), + }; + + const Style style_white { // level + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::white(), + }; + + const Style style_yellow { //Found signal + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::yellow(), + }; + + const Style style_green { //Found signal + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::green(), + }; + + const Style style_red { //erasing freq + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::red(), + }; + + const Style style_blue { // quick level, wait == 0 + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::blue(), + }; + + std::string title() const override { return "Level"; }; + + private: + NavigationView& nav_; + + size_t change_mode( freqman_index_t mod_type); + void on_statistics_update(const ChannelStatistics& statistics); + void set_display_freq( int64_t freq ); + bool check_sd_card(); + + int32_t db { 0 }; + long long int MAX_UFREQ = { 7200000000 }; // maximum usable freq + bool sd_card_mounted = false ; + rf::Frequency freq = { 0 } ; + + Labels labels + { + { { 0 * 8 , 0 * 16 }, "LNA: VGA: AMP: VOL: ", Color::light_grey() }, + { { 0 * 8 , 1 * 16 }, "BW: MODE: S: ", Color::light_grey() }, + }; + + LNAGainField field_lna { + { 4 * 8, 0 * 16 } + }; + + VGAGainField field_vga { + { 11 * 8, 0 * 16 } + }; + + RFAmpField field_rf_amp { + { 18 * 8, 0 * 16 } + }; + + NumberField field_volume { + { 24 * 8, 0 * 16 }, + 2, + { 0, 99 }, + 1, + ' ', + }; + + OptionsField field_bw { + { 3 * 8, 1 * 16 }, + 6, + { } + }; + + OptionsField field_mode { + { 15 * 8, 1 * 16 }, + 3, + { + } + }; + + OptionsField step_mode { + { 21 * 8, 1 * 16 }, + 12, + { + } + }; + + RSSIGraph rssi_graph { // 240x320 => + { 0 , 5 * 16 + 4 , 240 - 5 * 8 , 320 - 5 * 16 - 4 }, + }; + + RSSI rssi { // 240x320 => + { 240 - 5 * 8 , 5 * 16 + 4 , 5 * 8 , 320 - 5 * 16 - 4 }, + }; + + + ButtonWithEncoder button_frequency { + { 0 * 8 , 2 * 16 + 8 , 15 * 8 , 1 * 8 }, + "" + }; + + OptionsField audio_mode { + { 16 * 8, 2 * 16 + 4 }, + 9, + { + {"audio off", 0}, + {"audio on",1} + //{"tone on", 2}, + //{"tone off", 2}, + } + }; + + + Text text_ctcss { + { 20 * 8, 3 * 16 + 4 , 14 * 8, 1 * 8 }, + "" + }; + + Text freq_stats_rssi { + { 0 * 8 , 3 * 16 + 4 , 20 * 8, 16 }, + }; + + Text freq_stats_db { + { 0 * 8 , 4 * 16 + 4 , 15 * 8, 16 }, + }; + + + OptionsField peak_mode { + { 44 + 10 * 8, 4 * 16 + 4 }, + 10, + { + {"peak:none", 0}, + {"peak:0.25s",250}, + {"peak:0.5s",500}, + {"peak:1s",1000}, + {"peak:3s",3000}, + {"peak:5s",5000}, + {"peak:10s",10000}, + } + }; + + void handle_coded_squelch(const uint32_t value); + void on_headphone_volume_changed(int32_t v); + + MessageHandlerRegistration message_handler_coded_squelch { + Message::ID::CodedSquelch, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->handle_coded_squelch(message.value); + } + }; + + MessageHandlerRegistration message_handler_stats { + Message::ID::ChannelStatistics, + [this](const Message* const p) { + this->on_statistics_update(static_cast(p)->statistics); + } + }; + // app save settings + std::app_settings settings { }; + std::app_settings::AppSettings app_settings { }; + }; + +} /* namespace ui */ + +#endif diff --git a/firmware/application/ui/ui_rssi.cpp b/firmware/application/ui/ui_rssi.cpp index d99795ec0..674279dac 100644 --- a/firmware/application/ui/ui_rssi.cpp +++ b/firmware/application/ui/ui_rssi.cpp @@ -26,85 +26,286 @@ #include +#define min(a,b) ((a)<(b)?(a):(b)) +#define max(a,b) ((a)>(b)?(a):(b)) +#define abs(x) ((x)>0?(x):-(x)) + namespace ui { -void RSSI::paint(Painter& painter) { - const auto r = screen_rect(); + void RSSI::paint(Painter& painter) { + const auto r = screen_rect(); - constexpr int rssi_sample_range = 256; - constexpr float rssi_voltage_min = 0.4; - constexpr float rssi_voltage_max = 2.2; - constexpr float adc_voltage_max = 3.3; - constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; - constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; - constexpr int raw_delta = raw_max - raw_min; - const range_t x_avg_range { 0, r.width() - 1 }; - const auto x_avg = x_avg_range.clip((avg_ - raw_min) * r.width() / raw_delta); - const range_t x_min_range { 0, x_avg }; - const auto x_min = x_min_range.clip((min_ - raw_min) * r.width() / raw_delta); - const range_t x_max_range { x_avg + 1, r.width() }; - const auto x_max = x_max_range.clip((max_ - raw_min) * r.width() / raw_delta); + constexpr int rssi_sample_range = 256; + constexpr float rssi_voltage_min = 0.4; + constexpr float rssi_voltage_max = 2.2; + constexpr float adc_voltage_max = 3.3; + constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; + constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; + constexpr int raw_delta = raw_max - raw_min; - const Rect r0 { r.left(), r.top(), x_min, r.height() }; - painter.fill_rectangle( - r0, - Color::blue() - ); - const Rect r1 { r.left() + x_min, r.top(), x_avg - x_min, r.height() }; - painter.fill_rectangle( - r1, - Color::red() - ); + if( !vertical_rssi_enabled ) + { + // horizontal left to right level meters + const range_t x_avg_range { 0, r.width() - 1 }; + const auto x_avg = x_avg_range.clip((avg_ - raw_min) * r.width() / raw_delta); + const range_t x_min_range { 0, x_avg }; + const auto x_min = x_min_range.clip((min_ - raw_min) * r.width() / raw_delta); + const range_t x_max_range { x_avg + 1, r.width() - 1 }; + const auto x_max = x_max_range.clip((max_ - raw_min) * r.width() / raw_delta); + const auto peak = x_max_range.clip((peak_ - raw_min) * r.width() / raw_delta); - const Rect r2 { r.left() + x_avg, r.top(), 1, r.height() }; - painter.fill_rectangle( - r2, - Color::white() - ); + // x_min + const Rect r0 { r.left(), r.top(), x_min, r.height() }; + painter.fill_rectangle( + r0, + Color::blue() + ); - const Rect r3 { r.left() + x_avg + 1, r.top(), x_max - (x_avg + 1), r.height() }; - painter.fill_rectangle( - r3, - Color::red() - ); + // x_avg + const Rect r1 { r.left() + x_min, r.top(), x_avg - x_min, r.height() }; + painter.fill_rectangle( + r1, + Color::red() + ); - const Rect r4 { r.left() + x_max, r.top(), r.width() - x_max, r.height() }; - painter.fill_rectangle( - r4, - Color::black() - ); - - if (pitch_rssi_enabled) { - baseband::set_pitch_rssi((avg_ - raw_min) * 2000 / raw_delta, true); - } -} + // x_avg middle marker + const Rect r2 { r.left() + x_avg, r.top(), 1, r.height() }; + painter.fill_rectangle( + r2, + Color::white() + ); -int32_t RSSI::get_min() -{ - return min_ ; -} + // x_max + const Rect r3 { r.left() + x_avg + 1, r.top(), x_max - (x_avg + 1), r.height() }; + painter.fill_rectangle( + r3, + Color::red() + ); -int32_t RSSI::get_avg() -{ - return avg_ ; -} + // filling last part in black + const Rect r4 { r.left() + x_max, r.top(), r.width() - x_max, r.height() }; + painter.fill_rectangle( + r4, + Color::black() + ); -int32_t RSSI::get_max() -{ - return max_ ; -} + // show green peak value + if( peak_enabled ) + { + const Rect r5 { r.left(), r.bottom() + peak , r.width() , 1 }; + painter.fill_rectangle( + r5, + Color::green() + ); + } + } + else + { + // vertical bottom to top level meters + const range_t y_avg_range { 0, r.height() - 1 }; + const auto y_avg = y_avg_range.clip((avg_ - raw_min) * r.height() / raw_delta); + const range_t y_min_range { 0, y_avg }; + const auto y_min = y_min_range.clip((min_ - raw_min) * r.height() / raw_delta); + const range_t y_max_range { y_avg + 1, r.height() - 1 }; + const auto y_max = y_max_range.clip((max_ - raw_min) * r.height() / raw_delta); + const range_t peak_range { 0, r.height() - 1 }; + const auto peak = peak_range.clip((peak_ - raw_min) * r.height() / raw_delta); -void RSSI::set_pitch_rssi(bool enabled) { - pitch_rssi_enabled = enabled; - if (!enabled) baseband::set_pitch_rssi(0, false); -} + // y_min + const Rect r0 { r.left(), r.bottom() - y_min, r.width() , y_min }; + painter.fill_rectangle( + r0, + Color::blue() + ); -void RSSI::on_statistics_update(const RSSIStatistics& statistics) { - min_ = statistics.min; - avg_ = statistics.accumulator / statistics.count; - max_ = statistics.max; - set_dirty(); -} + // y_avg + const Rect r1 { r.left(), r.bottom() - y_avg , r.width() , y_avg - y_min }; + painter.fill_rectangle( + r1, + Color::red() + ); + // y_avg middle marker + const Rect r2 { r.left(), r.bottom() - y_avg , r.width() , 1 }; + painter.fill_rectangle( + r2, + Color::white() + ); + + // y_max + const Rect r3 { r.left(), r.bottom() - y_max , r.width() , y_max - y_avg }; + //const Rect r3 { r.left(), r.bottom() - y_max , r.width() , y_max - y_avg - 1 }; + painter.fill_rectangle( + r3, + Color::red() + ); + + // fill last part of level in black + const Rect r4 { r.left(), r.top() , r.width() , r.height() - y_max }; + painter.fill_rectangle( + r4, + Color::black() + ); + + // show green peak value if enabled + if( peak_enabled ) + { + const Rect r5 { r.left(), r.bottom() - peak , r.width() , 1 }; + painter.fill_rectangle( + r5, + Color::green() + ); + } + } + if (pitch_rssi_enabled) { + baseband::set_pitch_rssi((avg_ - raw_min) * 2000 / raw_delta, true); + } + } + + int32_t RSSI::get_min() + { + return min_ ; + } + + int32_t RSSI::get_avg() + { + return avg_ ; + } + + int32_t RSSI::get_max() + { + return max_ ; + } + + void RSSI::set_pitch_rssi(bool enabled) { + pitch_rssi_enabled = enabled; + if (!enabled) baseband::set_pitch_rssi(0, false); + } + + void RSSI::set_vertical_rssi(bool enabled) { + if( enabled ) + vertical_rssi_enabled = true ; + else + vertical_rssi_enabled = false ; + } + + void RSSI::set_peak(bool enabled, size_t duration) { + peak_enabled = enabled ; + peak_duration = duration ; + } + + void RSSI::on_statistics_update(const RSSIStatistics& statistics) { + min_ = statistics.min; + avg_ = statistics.accumulator / statistics.count; + max_ = statistics.max; + if( peak_enabled ) + { + peak_duration_ = peak_duration_ + 100 ; + if( max_ > peak_ ) + { + peak_ = max_ ; + } + if( peak_duration_ > peak_duration ) + { + peak_duration_ = 0 ; + peak_ = max_ ; + } + } + set_dirty(); + } + + + void RSSIGraph::paint(Painter& painter) { + const auto r = screen_rect(); + + for ( int n = 2 ; (unsigned)n <= graph_list.size(); n++) { + auto& entry = graph_list[graph_list.size()-n]; + auto& prev_entry = graph_list[graph_list.size()-(n-1)]; + + // black + const Point p0{ r.right() - n , r.top() }; + painter.draw_vline( + p0, + r.height(), + Color::black()); + + // y_max + int32_t top_y_val = max( entry.rssi_max , prev_entry.rssi_max ); + int32_t width_y = abs( entry.rssi_max - prev_entry.rssi_max ); + if( width_y == 0 ) + width_y = 1 ; + const Point p1{ r.right() - n , r.bottom() - top_y_val }; + painter.draw_vline( + p1, + width_y, + Color::red()); + + // y_avg + top_y_val = max( entry.rssi_avg , prev_entry.rssi_avg ); + width_y = abs( entry.rssi_avg - prev_entry.rssi_avg ); + if( width_y == 0 ) + width_y = 1 ; + const Point p2{ r.right() - n , r.bottom() - top_y_val }; + painter.draw_vline( + p2, + width_y, + Color::white()); + + // y_min + top_y_val = max( entry.rssi_min , prev_entry.rssi_min ); + width_y = abs( entry.rssi_min - prev_entry.rssi_min ); + if( width_y == 0 ) + width_y = 1 ; + const Point p3{ r.right() - n , r.bottom() - top_y_val }; + painter.draw_vline( + p3, + width_y, + Color::blue()); + + // hack to display db + top_y_val = max( entry.db , prev_entry.db ); + width_y = abs( entry.db - prev_entry.db ); + if( width_y == 0 ) + width_y = 1 ; + const Point p4{ r.right() - n , r.bottom() - top_y_val }; + painter.draw_vline( + p4, + width_y, + Color::green()); + } + } + + void RSSIGraph::add_values(int32_t rssi_min, int32_t rssi_avg, int32_t rssi_max, int32_t db ) + { + const auto r = screen_rect(); + + constexpr int rssi_sample_range = 256; + constexpr float rssi_voltage_min = 0.4; + constexpr float rssi_voltage_max = 2.2; + constexpr float adc_voltage_max = 3.3; + constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; + constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; + constexpr int raw_delta = raw_max - raw_min; + + // vertical bottom to top level meters + const range_t y_avg_range { 0, r.height() - 1 }; + const auto y_avg = y_avg_range.clip((rssi_avg - raw_min) * r.height() / raw_delta); + const range_t y_min_range { 0, y_avg }; + const auto y_min = y_min_range.clip((rssi_min - raw_min) * r.height() / raw_delta); + const range_t y_max_range { y_avg + 1, r.height() - 1 }; + const auto y_max = y_max_range.clip((rssi_max - raw_min) * r.height() / raw_delta); + const range_t db_range { -100 , 20 }; + auto db_ = db_range.clip( db ); + db_ = db_ - 20 ; + db_ = db_ * r.height() / 120 ; + db_ = r.height() + db_ ; + + graph_list . push_back( { y_min, y_avg, y_max , db_ } ); + if( graph_list.size() > (unsigned)r.width() ) + { + graph_list.erase( graph_list.begin() ); + } + set_dirty(); + } } /* namespace ui */ diff --git a/firmware/application/ui/ui_rssi.hpp b/firmware/application/ui/ui_rssi.hpp index f792e7937..f7783529d 100644 --- a/firmware/application/ui/ui_rssi.hpp +++ b/firmware/application/ui/ui_rssi.hpp @@ -49,13 +49,21 @@ public: int32_t get_min(); int32_t get_avg(); int32_t get_max(); + void set_vertical_rssi(bool enabled); + void set_peak(bool enabled, size_t duration); private: int32_t min_; int32_t avg_; int32_t max_; + int32_t peak_ = 0 ; + size_t peak_duration_ = 0 ; bool pitch_rssi_enabled = false; + bool vertical_rssi_enabled = false; // scale [vertically/from bottom to top] + // instead of [horizontally/from left to right] + bool peak_enabled = false; + size_t peak_duration = 1000; // peak duration in msec before being reset to actual max_rssi MessageHandlerRegistration message_handler_stats { Message::ID::RSSIStatistics, @@ -76,6 +84,29 @@ private: void set_pitch_rssi(bool enabled); }; +struct RSSIGraph_entry { + int32_t rssi_min { 0 }; + int32_t rssi_avg { 0 }; + int32_t rssi_max { 0 }; + int32_t db { 0 }; +}; + +using RSSIGraphList = std::vector; + +class RSSIGraph : public Widget { +public: + RSSIGraph( + const Rect parent_rect + ) : Widget { parent_rect } + { + } + void paint(Painter& painter) override; + void add_values(int32_t rssi_min, int32_t rssi_avg, int32_t rssi_max, int32_t db ); + +private: + RSSIGraphList graph_list { } ; +}; + } #endif/*__UI_RSSI_H__*/ diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 782468081..cb0feb662 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -58,6 +58,7 @@ #include "ui_scanner.hpp" #include "ui_search.hpp" #include "ui_recon.hpp" +#include "ui_level.hpp" #include "ui_sd_wipe.hpp" #include "ui_settings.hpp" #include "ui_siggen.hpp" @@ -484,6 +485,7 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) { { "Radiosnde", ui::Color::green(), &bitmap_icon_sonde, [&nav](){ nav.push(); } }, { "TPMS Cars", ui::Color::green(), &bitmap_icon_tpms, [&nav](){ nav.push(); } }, { "Recon", ui::Color::green(), &bitmap_icon_scanner, [&nav](){ nav.push(); } }, + { "Level", ui::Color::green(), &bitmap_icon_options_radio, [&nav](){ nav.push(); } }, { "APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav](){ nav.push(); } } /* { "DMR", ui::Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push(); } }, diff --git a/hackrf b/hackrf index b42a18550..ae71cb5b7 160000 --- a/hackrf +++ b/hackrf @@ -1 +1 @@ -Subproject commit b42a185503b306f390dc620b57cdc797217407de +Subproject commit ae71cb5b7ae29eb9bddf359896a66aaa88b9dfac