diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 9afbbd46..c620adc0 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -162,7 +162,7 @@ set(CPPSRC ui_baseband_stats_view.cpp ui_bht_tx.cpp ui_channel.cpp - ui_closecall.cpp + ui_scanner.cpp ui_coasterp.cpp ui_cw.cpp ui_debug.cpp diff --git a/firmware/application/analog_audio_app.cpp b/firmware/application/analog_audio_app.cpp index 0863c21a..57e7183b 100644 --- a/firmware/application/analog_audio_app.cpp +++ b/firmware/application/analog_audio_app.cpp @@ -77,8 +77,10 @@ NBFMOptionsView::NBFMOptionsView( /* AnalogAudioView *******************************************************/ AnalogAudioView::AnalogAudioView( - NavigationView& nav -) { + NavigationView& nav, + bool eos +) : nav_ (nav) +{ add_children({ &rssi, &channel, @@ -91,6 +93,8 @@ AnalogAudioView::AnalogAudioView( &record_view, &waterfall, }); + + exit_on_squelch = eos; field_frequency.set_value(receiver_model.tuning_frequency()); field_frequency.set_step(receiver_model.frequency_step()); @@ -311,4 +315,8 @@ void AnalogAudioView::update_modulation(const ReceiverModel::Mode modulation) { } } +void AnalogAudioView::squelched() { + if (exit_on_squelch) nav_.pop(); +} + } /* namespace ui */ diff --git a/firmware/application/analog_audio_app.hpp b/firmware/application/analog_audio_app.hpp index 566b63d9..a8292516 100644 --- a/firmware/application/analog_audio_app.hpp +++ b/firmware/application/analog_audio_app.hpp @@ -82,7 +82,7 @@ private: class AnalogAudioView : public View { public: - AnalogAudioView(NavigationView& nav); + AnalogAudioView(NavigationView& nav, bool eos); ~AnalogAudioView(); void on_hide() override; @@ -96,6 +96,9 @@ private: const Rect options_view_rect { 0 * 8, 1 * 16, 30 * 8, 1 * 16 }; + NavigationView& nav_; + bool exit_on_squelch { false }; + RSSI rssi { { 21 * 8, 0, 6 * 8, 4 }, }; @@ -163,6 +166,17 @@ private: void set_options_widget(std::unique_ptr new_widget); void update_modulation(const ReceiverModel::Mode modulation); + + void squelched(); + + MessageHandlerRegistration message_handler_squelch_signal { + Message::ID::RequestSignal, + [this](const Message* const p) { + (void)p; + this->squelched(); + } + }; + }; } /* namespace ui */ diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp index 3ef5d8ed..cab3857b 100755 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -23,14 +23,15 @@ // Color bitmaps generated with: // Gimp image > indexed colors (16), then "xxd -i *.bmp" -//TODO: Waveform widget as FFT view in scanner +//TODO: Scanner multiple slices (buggy) +//TODO: Use to_string_short_freq +//TODO: Add auto-rounding to 12.5kHz channel option in scanner +//TODO: Waveform widget as FFT view in scanner ? //BUG: Replay freezes when SD card not present //BUG: RDS doesn't stop baseband when stopping tx ? -//BUG: Check AFSK transmit end, skips last bits ? - +//TEST: Check AFSK transmit end, skips last bits ? //TEST: Imperial in whipcalc -//TODO: IQ replay //TODO: Optimize (and group ?) CTCSS tone gen code //TODO: Morse use prosigns //TODO: Morse live keying mode ? @@ -41,39 +42,35 @@ Continuous (Fox-oring) 60s transmit, 360s space (Classic 1/7 min) */ //TODO: Use transmittermodel bw setting -//TODO: Use Labels widget wherever possible -//TODO: Use TransmitterView in TEDI/LCR, Numbers, whistle, ... +//TODO: Use TransmitterView in TEDI/LCR, Numbers, ... //TODO: FreqMan: Add and rename categories //TODO: FreqMan: Sort by category in edit screen //TODO: FreqMan: Cap entry count per category (only done for total entries right now) -//TODO: Script engine ? -//TODO: Close Call multiple slices (buggy) //TODO: Wav visualizer - //TODO: File browser view ? //TODO: Mousejack ? //TODO: Move frequencykeypad from ui_receiver to ui_widget (used everywhere) //TODO: ADS-B draw trajectory + GPS coordinates + scale, and playback -//TODO: Analog TV tx with camcorder font character generator -//TODO: Make Whistle use proc_tones //TODO: RDS multiple groups (sequence) //TODO: Use ModalMessageView confirmation for TX ? -//TODO: Show address/data bit fields in OOK TX -//TODO: Scan for OOK TX -//TODO: Check more OOK encoders //TODO: Use msgpack for settings, lists... on sd card -//Multimon-style stuff: -//TODO: AFSK receiver +// Multimon-style stuff: //TODO: CTCSS detector //TODO: DMR detector //TODO: GSM channel detector //TODO: SIGFOX RX/TX -//TODO: Bodet :) -//TODO: LCR full message former (see norm) -//TODO: AFSK NRZI //TODO: Playdead amnesia and login //TODO: Setup: Play dead by default ? Enable/disable ? + +// Old or low-priority stuff: +//TODO: Bodet :) +//TODO: Analog TV tx with camcorder font character generator +//TODO: Show address/data bit fields in OOK TX +//TODO: Scan for OOK TX +//TODO: Script engine ? +//TODO: AFSK receiver +//TODO: Check more OOK encoders //BUG (fixed ?): No audio in about when shown second time //TODO: Show MD5 mismatches for modules not found, etc... //TODO: Module name/filename in modules.hpp to indicate requirement in case it's not found ui_loadmodule diff --git a/firmware/application/ui_closecall.cpp b/firmware/application/ui_closecall.cpp deleted file mode 100644 index c664ea47..00000000 --- a/firmware/application/ui_closecall.cpp +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. - * Copyright (C) 2016 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_closecall.hpp" - -#include "rtc_time.hpp" -#include "event_m0.hpp" -#include "portapack.hpp" -#include "baseband_api.hpp" -#include "string_format.hpp" - -#include -#include -#include - -using namespace portapack; - -namespace ui { - -void CloseCallView::focus() { - field_frequency_min.focus(); -} - -CloseCallView::~CloseCallView() { - rtc_time::signal_tick_second -= signal_token_tick_second; - receiver_model.disable(); - baseband::shutdown(); -} - -void CloseCallView::do_detection() { - uint8_t xmax = 0; - int16_t imax = 0; - uint16_t iraw = 0, c; - uint8_t power; - rf::Frequency freq_low, freq_high; - - mean /= (CC_BIN_NB_NO_DC * slices_max); - - // Find max value over threshold for all slices - for (c = 0; c < slices_max; c++) { - power = slicemax_pow[c]; - if (power >= min_threshold) { - if ((power - min_threshold >= mean) && (power > xmax)) { - xmax = power; - imax = slicemax_idx[c] + (c * CC_BIN_NB); - iraw = slicemax_idx[c]; - } - } - } - - // Lock / release - if ((imax >= last_channel - 2) && (imax <= last_channel + 2) && (imax)) { - // Staying around the same frequency - if (detect_counter >= (5 / slices_max)) { // Todo: ugly, change. Should depend on refresh rate. - if ((imax != locked_imax) || (!locked)) { - std::string finalstr; - - if (locked) { - // Already locked, adjust - if (weight < 16) { - frequency_acc += (slice_frequency + (CC_BIN_WIDTH * (imax - 120))); // Average - weight++; - } - } else { - // Lost, locking - frequency_acc = slice_frequency + (CC_BIN_WIDTH * (imax - 120)); // Init - if ((frequency_acc >= f_min) && (frequency_acc <= f_max)) { - - text_infos.set("Locked !"); - big_display.set_style(&style_locked); - - // Approximation/error display - freq_low = (frequency_acc - 4883) / 1000; - freq_high = (frequency_acc + 4883) / 1000; - finalstr = "~9.8kHz: " + to_string_dec_uint(freq_low / 1000) + "." + to_string_dec_uint(freq_low % 1000); - finalstr += "/" + to_string_dec_uint(freq_high / 1000) + "." + to_string_dec_uint(freq_high % 1000); - text_precision.set(finalstr); - - locked = true; - weight = 1; - locked_imax = imax; - } - } - - resolved_frequency = frequency_acc / weight; - big_display.set(resolved_frequency); - } - release_counter = 0; - } else { - detect_counter++; - } - } else { - detect_counter = 0; - if (locked) { - if (release_counter == 6) { - locked = false; - text_infos.set("Lost"); - big_display.set_style(&style_grey); - //big_display.set(resolved_frequency); - } else { - release_counter++; - } - } - } - - last_channel = imax; - scan_counter++; - - portapack::display.fill_rectangle({last_pos, 90, 1, 6}, Color::black()); - if (iraw < 120) - iraw += 2; - else - iraw -= 0; - last_pos = (ui::Coord)iraw; - portapack::display.fill_rectangle({last_pos, 90, 1, 6}, Color::red()); -} - -void CloseCallView::on_channel_spectrum(const ChannelSpectrum& spectrum) { - uint8_t xmax = 0; - int16_t imax = 0; - uint8_t power; - size_t i, m; - std::array pixel_row; - - baseband::spectrum_streaming_stop(); - - // Draw spectrum line (for debug) - for (i = 0; i < 120; i++) { - const auto pixel_color = spectrum_rgb3_lut[spectrum.db[134 + i]]; // 134~253 in 0~119 - pixel_row[i] = pixel_color; - } - for (i = 120; i < 240; i++) { - const auto pixel_color = spectrum_rgb3_lut[spectrum.db[i - 118]]; // 2~121 in 120~239 - pixel_row[i] = pixel_color; - } - display.draw_pixels( - { { 0, 96 + slices_counter * 4 }, { pixel_row.size(), 1 } }, - pixel_row - ); - - // Find max for this slice: - - // Check if left of slice needs to be trimmed (masked) - //if (slices_counter == 0) - // i = slice_trim; - //else - i = 0; - for ( ; i < 120; i++) { - power = spectrum.db[134 + i]; - mean += power; - if (power > xmax) { - xmax = power; - imax = i - 2; - } - } - // Check if right of slice needs to be trimmed (masked) - //if (slices_counter == (slices_max - 1)) - // m = 240 - slice_trim; - //else - m = 240; - for (i = 120; i < m; i++) { - power = spectrum.db[i - 118]; - mean += power; - if (power > xmax) { - xmax = power; - imax = i + 2; - } - } - - slicemax_pow[slices_counter] = xmax; - slicemax_idx[slices_counter] = imax; - - // Slice update - if (slicing) { - if (slices_counter >= (slices_max - 1)) { - do_detection(); - mean = 0; - slices_counter = 0; - } else { - slices_counter++; - } - slice_frequency = slice_start + (slices_counter * CC_SLICE_WIDTH); - receiver_model.set_tuning_frequency(slice_frequency); - } else { - do_detection(); - } - - baseband::spectrum_streaming_start(); -} - -void CloseCallView::on_show() { - baseband::spectrum_streaming_start(); -} - -void CloseCallView::on_hide() { - baseband::spectrum_streaming_stop(); -} - -void CloseCallView::on_range_changed() { - rf::Frequency slices_span; - rf::Frequency slice_width; - int64_t offset; - - f_max = field_frequency_max.value(); - f_min = field_frequency_min.value(); - scan_span = abs(f_max - f_min); - - if (scan_span > CC_SLICE_WIDTH) { - // ex: 100~115 (15): 102.5(97.5~107.5) -> 112.5(107.5~117.5) = 2.5 lost left and right - slices_max = (scan_span + CC_SLICE_WIDTH - 1) / CC_SLICE_WIDTH; - slices_span = slices_max * CC_SLICE_WIDTH; - offset = ((scan_span - slices_span) / 2) + (CC_SLICE_WIDTH / 2); - slice_start = std::min(f_min, f_max) + offset; - slice_trim = 0; - slicing = true; - - // Todo: trims - } else { - slice_frequency = (f_max + f_min) / 2; - receiver_model.set_tuning_frequency(slice_frequency); - - slice_width = abs(f_max - f_min); - slice_trim = (240 - (slice_width * 240 / CC_SLICE_WIDTH)) / 2; - - portapack::display.fill_rectangle({0, 97, 240, 4}, Color::black()); - portapack::display.fill_rectangle({0, 97, slice_trim, 4}, Color::orange()); - portapack::display.fill_rectangle({240 - slice_trim, 97, slice_trim, 4}, Color::orange()); - - slices_max = 1; - slices_counter = 0; - slicing = false; - } - - /* - f_min = field_frequency_min.value(); - scan_span = 3000000; - slice_frequency = (f_min + 1500000); - slice_start = slice_frequency; - receiver_model.set_tuning_frequency(slice_frequency); - slice_trim = 0; - slices_max = 1; - slices_counter = 0; - slicing = false; - field_frequency_max.set_value(f_min + 3000000); -*/ - - text_slices.set(to_string_dec_int(slices_max)); - slices_counter = 0; -} - -void CloseCallView::on_lna_changed(int32_t v_db) { - receiver_model.set_lna(v_db); -} - -void CloseCallView::on_vga_changed(int32_t v_db) { - receiver_model.set_vga(v_db); -} - -void CloseCallView::on_tick_second() { - // Update scan rate indication - text_rate.set(to_string_dec_uint(scan_counter, 3)); - scan_counter = 0; -} - -CloseCallView::CloseCallView( - NavigationView& 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_slices, - &text_rate, - &text_mhz, - &text_infos, - &text_precision, - &text_debug, - &big_display, - &button_exit - }); - - text_slices.set_style(&style_grey); - text_rate.set_style(&style_grey); - text_mhz.set_style(&style_grey); - big_display.set_style(&style_grey); - - baseband::set_spectrum(CC_SLICE_WIDTH, 32); - - field_threshold.set_value(80); - field_threshold.on_change = [this](int32_t v) { - min_threshold = v; - }; - - field_frequency_min.set_value(receiver_model.tuning_frequency()); - 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.tuning_frequency()); - new_view->on_changed = [this](rf::Frequency f) { - //this->on_range_changed(); - this->field_frequency_min.set_value(f); - }; - }; - - field_frequency_max.set_value(receiver_model.tuning_frequency() + 2000000); - 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.tuning_frequency()); - new_view->on_changed = [this](rf::Frequency f) { - //this->on_range_changed(); - 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); - }; - - on_range_changed(); - - button_exit.on_select = [&nav](Button&){ - nav.pop(); - }; - - signal_token_tick_second = rtc_time::signal_tick_second += [this]() { - this->on_tick_second(); - }; - - receiver_model.set_modulation(ReceiverModel::Mode::SpectrumAnalysis); - receiver_model.set_sampling_rate(CC_SLICE_WIDTH); - receiver_model.set_baseband_bandwidth(2500000); - receiver_model.enable(); -} - -} /* namespace ui */ diff --git a/firmware/application/ui_closecall.hpp b/firmware/application/ui_closecall.hpp deleted file mode 100644 index 836c2011..00000000 --- a/firmware/application/ui_closecall.hpp +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. - * Copyright (C) 2016 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 "receiver_model.hpp" - -#include "spectrum_color_lut.hpp" - -#include "ui_receiver.hpp" -#include "ui_font_fixed_8x16.hpp" - -namespace ui { - -#define CC_SLICE_WIDTH 2500000 // Radio bandwidth -#define CC_BIN_NB 256 // Total power bins (skip 4 at center, 2*6 on sides) -#define CC_BIN_NB_NO_DC (CC_BIN_NB - 16) -#define CC_BIN_WIDTH (CC_SLICE_WIDTH / CC_BIN_NB) - -class CloseCallView : public View { -public: - CloseCallView(NavigationView& nav); - ~CloseCallView(); - - CloseCallView(const CloseCallView&) = delete; - CloseCallView(CloseCallView&&) = delete; - CloseCallView& operator=(const CloseCallView&) = delete; - CloseCallView& operator=(CloseCallView&&) = delete; - - void on_show() override; - void on_hide() override; - void focus() override; - - std::string title() const override { return "Close Call"; }; - -private: - const Style style_grey { // For labels and lost signal - .font = font::fixed_8x16, - .background = Color::black(), - .foreground = Color::grey(), - }; - - const Style style_locked { - .font = font::fixed_8x16, - .background = Color::black(), - .foreground = Color::green(), - }; - - rf::Frequency f_min { 0 }, f_max { 0 }; - Coord last_pos { 0 }; - ChannelSpectrumFIFO* fifo { nullptr }; - uint8_t detect_counter { 0 }, release_counter { 0 }; - uint8_t slice_trim { 0 }; - uint32_t mean { 0 }; - uint32_t min_threshold { 80 }; // Todo: Put this in persistent / settings - rf::Frequency slice_start { 0 }; - rf::Frequency slice_frequency { 0 }; - uint8_t slices_max { 0 }; - uint8_t slices_counter { 0 }; - int16_t last_channel { 0 }; - uint32_t weight { 0 }; - int64_t frequency_acc { 0 }; - rf::Frequency scan_span { 0 }, resolved_frequency { 0 }; - uint16_t locked_imax { 0 }; - uint8_t slicemax_pow[32]; // Todo: Cap max slices ! - int16_t slicemax_idx[32]; - uint8_t scan_counter { 0 }; - SignalToken signal_token_tick_second { }; - bool ignore { true }; - bool slicing { false }; - bool locked { false }; - - 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 on_tick_second(); - - /* |012345678901234567890123456789| - * | Min: Max: LNA VGA | - * | 0000.0000 0000.0000 00 00 | - * | Threshold: 000 | - * | Slices: 00 Rate: 00Hz | - * | - * */ - - Labels labels { - { { 1 * 8, 0 }, "Min: Max: LNA VGA", Color::light_grey() }, - { { 1 * 8, 4 * 8 }, "Threshold:", Color::light_grey() }, - { { 1 * 8, 6 * 8 }, "Slices: Rate: Hz", Color::light_grey() } - }; - - NumberField field_threshold { - { 12 * 8, 2 * 16 }, - 3, - { 5, 250 }, - 5, - ' ' - }; - - FrequencyField field_frequency_min { - { 1 * 8, 1 * 16 }, - }; - FrequencyField field_frequency_max { - { 11 * 8, 1 * 16 }, - }; - - LNAGainField field_lna { - { 22 * 8, 1 * 16 } - }; - VGAGainField field_vga { - { 26 * 8, 1 * 16 } - }; - - Text text_slices { - { 9 * 8, 3 * 16, 2 * 8, 16 }, - "--" - }; - Text text_rate { - { 24 * 8, 3 * 16, 2 * 8, 16 }, - "--" - }; - - Text text_infos { - { 1 * 8, 6 * 16, 28 * 8, 16 }, - "..." - }; - - BigFrequency big_display { - { 4, 8 * 16, 28 * 8, 32 }, - 0 - }; - - Text text_mhz { - { 26 * 8, 12 * 16 - 4, 3 * 8, 16 }, - "MHz" - }; - - Text text_precision { - { 1 * 8, 13 * 16, 28 * 8, 16 }, - "..." - }; - - Text text_debug { - { 1 * 8, 14 * 16, 28 * 8, 16 }, - "DEBUG: -" - }; - - Button button_exit { - { 92, 264, 56, 32 }, - "Exit" - }; - - MessageHandlerRegistration message_handler_spectrum_config { - Message::ID::ChannelSpectrumConfig, - [this](const Message* const p) { - const auto message = *reinterpret_cast(p); - this->fifo = message.fifo; - } - }; - MessageHandlerRegistration message_handler_frame_sync { - Message::ID::DisplayFrameSync, - [this](const Message* const) { - if( this->fifo ) { - ChannelSpectrum channel_spectrum; - while( fifo->out(channel_spectrum) ) { - this->on_channel_spectrum(channel_spectrum); - } - } - } - }; -}; - -} /* namespace ui */ diff --git a/firmware/application/ui_mictx.hpp b/firmware/application/ui_mictx.hpp index 1aeb1b38..63df9cbb 100644 --- a/firmware/application/ui_mictx.hpp +++ b/firmware/application/ui_mictx.hpp @@ -90,7 +90,8 @@ private: VuMeter vumeter { { 1 * 8, 2 * 8, 5 * 8, 26 * 8 }, - 16 + 16, + false }; OptionsField options_gain { diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index b1beee4d..88df8a52 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -32,7 +32,6 @@ #include "ui_about.hpp" #include "ui_adsbtx.hpp" #include "ui_bht_tx.hpp" -#include "ui_closecall.hpp" #include "ui_coasterp.hpp" #include "ui_cw.hpp" #include "ui_debug.hpp" @@ -48,6 +47,7 @@ #include "ui_pocsag_tx.hpp" #include "ui_rds.hpp" #include "ui_sd_wipe.hpp" +#include "ui_scanner.hpp" #include "ui_setup.hpp" #include "ui_soundboard.hpp" #include "ui_sstvtx.hpp" @@ -293,7 +293,7 @@ TranspondersMenuView::TranspondersMenuView(NavigationView& nav) { ReceiverMenuView::ReceiverMenuView(NavigationView& nav) { add_items<6>({ { // { "AFSK", ui::Color::grey(), nullptr, [&nav](){ nav.push(); } }, // AFSKRXView - { "Audio", ui::Color::green(), nullptr, [&nav](){ nav.push(); } }, + { "Audio", ui::Color::green(), nullptr, [&nav](){ nav.push(false); } }, { "CCIR", ui::Color::grey(), nullptr, [&nav](){ nav.push(); } }, { "Nordic/BTLE", ui::Color::grey(), &bitmap_icon_nordic, [&nav](){ nav.push(); } }, { "POCSAG", ui::Color::cyan(), &bitmap_icon_pocsag, [&nav](){ nav.push(); } }, @@ -365,7 +365,7 @@ SystemMenuView::SystemMenuView(NavigationView& nav) { { "Audio transmitters", ui::Color::green(), &bitmap_icon_audiotx, [&nav](){ nav.push(); } }, { "Code transmitters", ui::Color::green(), &bitmap_icon_codetx, [&nav](){ nav.push(); } }, { "SSTV transmitter", ui::Color::dark_green(), &bitmap_icon_sstv, [&nav](){ nav.push(); } }, - { "Close Call", ui::Color::cyan(), &bitmap_icon_closecall, [&nav](){ nav.push(); } }, + { "Scanner/search", ui::Color::cyan(), &bitmap_icon_closecall, [&nav](){ nav.push(); } }, { "Jammer", ui::Color::orange(),&bitmap_icon_jammer, [&nav](){ nav.push(); } }, { "Utilities", ui::Color::purple(),&bitmap_icon_utilities, [&nav](){ nav.push(); } }, { "Setup", ui::Color::white(), &bitmap_icon_setup, [&nav](){ nav.push(); } }, diff --git a/firmware/application/ui_scanner.cpp b/firmware/application/ui_scanner.cpp new file mode 100644 index 00000000..ccb0fb5e --- /dev/null +++ b/firmware/application/ui_scanner.cpp @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 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_scanner.hpp" + +#include "baseband_api.hpp" +#include "string_format.hpp" + +using namespace portapack; + +namespace ui { + +template<> +void RecentEntriesTable::draw( + const Entry& entry, + const Rect& target_rect, + Painter& painter, + const Style& style +) { + std::string str_duration = ""; + + if (entry.duration < 600) + str_duration = to_string_dec_uint(entry.duration / 10) + "." + to_string_dec_uint(entry.duration % 10) + "s"; + else + str_duration = to_string_dec_uint(entry.duration / 600) + "m" + to_string_dec_uint((entry.duration / 10) % 60) + "s"; + + str_duration.resize(target_rect.width() / 8, ' '); + + painter.draw_string(target_rect.location(), style, to_string_short_freq(entry.frequency) + " " + entry.time + " " + str_duration); +} + +void ScannerView::focus() { + field_frequency_min.focus(); +} + +ScannerView::~ScannerView() { + receiver_model.disable(); + baseband::shutdown(); +} + +void ScannerView::do_detection() { + uint8_t power_max = 0; + int32_t bin_max = -1; + uint32_t bin_max_pixel = 0; + uint8_t power; + rtc::RTC datetime; + std::string str_approx, str_timestamp; + + mean_power = mean_acc / (SCAN_BIN_NB_NO_DC * slices_nb); + mean_acc = 0; + + overall_power_max = 0; + + // Find max power over threshold for all slices + for (size_t slice = 0; slice < slices_nb; slice++) { + power = slices[slice].max_power; + if (power > overall_power_max) + overall_power_max = power; + + if ((power >= mean_power + power_threshold) && (power > power_max)) { + power_max = power; + bin_max_pixel = slices[slice].max_index; + bin_max = bin_max_pixel + (slice * SCAN_BIN_NB); + } + } + + // Lock / release + if ((bin_max >= last_bin - 2) && (bin_max <= last_bin + 2) && (bin_max > -1)) { + + // Staying around the same bin + if (detect_timer >= DETECT_DELAY) { + if ((bin_max != locked_bin) || (!locked)) { + + if (!locked) { + resolved_frequency = slices[slice_counter].center_frequency + (SCAN_BIN_WIDTH * (bin_max - 120)); // Init + + // Check range + if ((resolved_frequency >= f_min) && (resolved_frequency <= f_max)) { + + duration = 0; + + auto& entry = ::on_packet(recent, resolved_frequency); + + rtcGetTime(&RTCD1, &datetime); + str_timestamp = to_string_dec_uint(datetime.hour(), 2, '0') + ":" + + to_string_dec_uint(datetime.minute(), 2, '0') + ":" + + to_string_dec_uint(datetime.second(), 2, '0'); + entry.set_time(str_timestamp); + recent_entries_view.set_dirty(); + + text_infos.set("Locked ! "); + big_display.set_style(&style_locked); + + // Approximation/error display + str_approx = "." + to_string_dec_uint(((resolved_frequency - 4883) / 1000) % 10000); + str_approx += "~." + to_string_dec_uint(((resolved_frequency + 4883) / 1000) % 10000); + text_approx.set(str_approx); + + locked = true; + locked_bin = bin_max; + + // TODO + /*nav_.pop(); + receiver_model.disable(); + baseband::shutdown(); + nav_.pop();*/ + + /*if (options_goto.selected_index() == 1) + nav_.push(false); + else if (options_goto.selected_index() == 2) + nav_.push(); + */ + } + } + + big_display.set(resolved_frequency); + } + } + release_timer = 0; + } else { + detect_timer = 0; + if (locked) { + if (release_timer >= RELEASE_DELAY) { + locked = false; + + auto& entry = ::on_packet(recent, resolved_frequency); + entry.set_duration(duration); + recent_entries_view.set_dirty(); + + text_infos.set("Listening"); + big_display.set_style(&style_grey); + } + } + } + + last_bin = bin_max; + scan_counter++; + + // Refresh red tick + portapack::display.fill_rectangle({last_pos, 90, 1, 6}, Color::black()); + if (bin_max > -1) { + if (bin_max_pixel < 120) + bin_max_pixel += 2; + //else + // bin_max_pixel -= 0; + last_pos = (ui::Coord)bin_max_pixel; + portapack::display.fill_rectangle({last_pos, 90, 1, 6}, Color::red()); + } +} + +void ScannerView::on_channel_spectrum(const ChannelSpectrum& spectrum) { + uint8_t power_max = 0; + int16_t bin_max = 0; + uint8_t power; + size_t bin; + std::array pixel_row; + + baseband::spectrum_streaming_stop(); + + // Draw spectrum line + for (bin = 0; bin < 120; bin++) { + const auto pixel_color = spectrum_rgb3_lut[spectrum.db[134 + bin]]; // 134~253 in 0~119 + pixel_row[bin] = pixel_color; + } + for (bin = 120; bin < 240; bin++) { + const auto pixel_color = spectrum_rgb3_lut[spectrum.db[bin - 118]]; // 2~121 in 120~239 + pixel_row[bin] = pixel_color; + } + display.draw_pixels( + { { 0, 88 + slice_counter * 2 }, { pixel_row.size(), 1 } }, + pixel_row + ); + + // Find max power for this slice + for (bin = 0 ; bin < 120; bin++) { + power = spectrum.db[134 + bin]; + mean_acc += power; + if (power > power_max) { + power_max = power; + bin_max = bin - 2; + } + } + for (bin = 120; bin < 240; bin++) { + power = spectrum.db[bin - 118]; + mean_acc += power; + if (power > power_max) { + power_max = power; + bin_max = bin + 2; + } + } + + slices[slice_counter].max_power = power_max; + slices[slice_counter].max_index = bin_max; + + // Slice update + if (slices_nb > 1) { + slice_counter++; + if (slice_counter >= slices_nb) { + do_detection(); + slice_counter = 0; + } + receiver_model.set_tuning_frequency(slices[slice_counter].center_frequency); + } else { + do_detection(); + } + + baseband::spectrum_streaming_start(); +} + +void ScannerView::on_show() { + baseband::spectrum_streaming_start(); +} + +void ScannerView::on_hide() { + baseband::spectrum_streaming_stop(); +} + +void ScannerView::on_range_changed() { + rf::Frequency slices_span, center_frequency; + int64_t offset; + size_t slice; + + f_min = field_frequency_min.value(); + f_max = field_frequency_max.value(); + scan_span = abs(f_max - f_min); + + if (scan_span > SCAN_SLICE_WIDTH) { + // ex: 100M~115M (15M span): + // slices_nb = (115M-100M)/2.5M = 6 + slices_nb = (scan_span + SCAN_SLICE_WIDTH - 1) / SCAN_SLICE_WIDTH; + if (slices_nb > 32) { + text_slices.set("!!"); + slices_nb = 32; + } else { + text_slices.set(to_string_dec_uint(slices_nb)); + } + // slices_span = 6 * 2.5M = 15M + slices_span = slices_nb * SCAN_SLICE_WIDTH; + // offset = 0 + 2.5/2 = 1.25M + offset = ((scan_span - slices_span) / 2) + (SCAN_SLICE_WIDTH / 2); + // slice_start = 100M + 1.25M = 101.25M + center_frequency = std::min(f_min, f_max) + offset; + + for (slice = 0; slice < slices_nb; slice++) { + slices[slice].center_frequency = center_frequency; + center_frequency += SCAN_SLICE_WIDTH; + } + } else { + slices[0].center_frequency = (f_max + f_min) / 2; + receiver_model.set_tuning_frequency(slices[0].center_frequency); + + slices_nb = 1; + text_slices.set("1"); + } + + slice_counter = 0; +} + +void ScannerView::on_lna_changed(int32_t v_db) { + receiver_model.set_lna(v_db); +} + +void ScannerView::on_vga_changed(int32_t v_db) { + receiver_model.set_vga(v_db); +} + +void ScannerView::do_timers() { + + if (timing_div >= 60) { + // ~1Hz + + timing_div = 0; + + // Update scan rate + text_rate.set(to_string_dec_uint(scan_counter, 3)); + scan_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++; +} + +ScannerView::ScannerView( + 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_goto, + //&options_goto, + &big_display, + &recent_entries_view, + &text_approx + }); + + recent_entries_view.set_parent_rect({ 0, 28 * 8, 240, 12 * 8 }); + + text_mean.set_style(&style_grey); + text_slices.set_style(&style_grey); + text_rate.set_style(&style_grey); + progress_timers.set_style(&style_grey); + big_display.set_style(&style_grey); + + baseband::set_spectrum(SCAN_SLICE_WIDTH, 32); + + //options_goto.set_selected_index(0); // Nothing + + field_threshold.set_value(80); + field_threshold.on_change = [this](int32_t value) { + power_threshold = value; + }; + + field_frequency_min.set_value(receiver_model.tuning_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.tuning_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + this->field_frequency_min.set_value(f); + }; + }; + + field_frequency_max.set_value(receiver_model.tuning_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.tuning_frequency()); + 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.set_modulation(ReceiverModel::Mode::SpectrumAnalysis); + receiver_model.set_sampling_rate(SCAN_SLICE_WIDTH); + receiver_model.set_baseband_bandwidth(2500000); + receiver_model.enable(); +} + +} /* namespace ui */ diff --git a/firmware/application/ui_scanner.hpp b/firmware/application/ui_scanner.hpp new file mode 100644 index 00000000..7f2b67b7 --- /dev/null +++ b/firmware/application/ui_scanner.hpp @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 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 "receiver_model.hpp" + +#include "spectrum_color_lut.hpp" + +#include "ui_receiver.hpp" +#include "ui_font_fixed_8x16.hpp" +#include "recent_entries.hpp" + +namespace ui { + +#define SCAN_SLICE_WIDTH 2500000 // Scan slice bandwidth +#define SCAN_BIN_NB 256 // FFT power bins (skip 4 at center, 2*6 on sides) +#define SCAN_BIN_NB_NO_DC (SCAN_BIN_NB - 16) // Bins after trimming +#define SCAN_BIN_WIDTH (SCAN_SLICE_WIDTH / SCAN_BIN_NB) + +#define DETECT_DELAY 5 // In 100ms units +#define RELEASE_DELAY 6 + +struct ScannerRecentEntry { + using Key = rf::Frequency; + + static constexpr Key invalid_key = 0xffffffff; + + rf::Frequency frequency; + uint32_t duration { 0 }; // In 100ms units + std::string time { "" }; + + ScannerRecentEntry( + ) : ScannerRecentEntry { 0 } + { + } + + ScannerRecentEntry( + const rf::Frequency frequency + ) : frequency { frequency } + { + } + + Key key() const { + return frequency; + } + + void set_time(std::string& new_time) { + time = new_time; + } + + void set_duration(uint32_t new_duration) { + duration = new_duration; + } +}; + +using ScannerRecentEntries = RecentEntries; + +class ScannerView : public View { +public: + ScannerView(NavigationView& nav); + ~ScannerView(); + + ScannerView(const ScannerView&) = delete; + ScannerView(ScannerView&&) = delete; + ScannerView& operator=(const ScannerView&) = delete; + ScannerView& operator=(ScannerView&&) = delete; + + void on_show() override; + void on_hide() override; + void focus() override; + + std::string title() const override { return "Close Call"; }; + +private: + NavigationView& nav_; + + const Style style_grey { // For informations and lost signal + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::grey(), + }; + + const Style style_locked { + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::green(), + }; + + struct slice_t { + rf::Frequency center_frequency; + uint8_t max_power; + int16_t max_index; + uint8_t power; + int16_t index; + } slices[32]; + + 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 }; + Coord last_pos { 0 }; + rf::Frequency scan_span { 0 }, resolved_frequency { 0 }; + uint16_t locked_bin { 0 }; + uint8_t scan_counter { 0 }; + bool locked { false }; + + 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(); + + const RecentEntriesColumns columns { { + { "Frequency", 9 }, + { "Time", 8 }, + { "Duration", 11 } + } }; + ScannerRecentEntries recent { }; + RecentEntriesView> recent_entries_view { columns, recent }; + + Labels labels { + { { 1 * 8, 0 }, "Min: Max: LNA VGA", Color::light_grey() }, + { { 1 * 8, 4 * 8 }, "Trig: /255 Mean: /255", Color::light_grey() }, + { { 1 * 8, 6 * 8 }, "Slices: /32 Rate: Hz", Color::light_grey() }, + { { 6 * 8, 10 * 8 }, "Timer Status", Color::light_grey() }, + { { 1 * 8, 25 * 8 }, "+/-4.9kHz:", Color::light_grey() }, + { { 26 * 8, 25 * 8 }, "MHz", Color::light_grey() } + }; + + NumberField field_threshold { + { 6 * 8, 2 * 16 }, + 3, + { 5, 255 }, + 5, + ' ' + }; + + FrequencyField field_frequency_min { + { 1 * 8, 1 * 16 }, + }; + FrequencyField field_frequency_max { + { 11 * 8, 1 * 16 }, + }; + LNAGainField field_lna { + { 22 * 8, 1 * 16 } + }; + VGAGainField field_vga { + { 26 * 8, 1 * 16 } + }; + + Text text_mean { + { 22 * 8, 2 * 16, 3 * 8, 16 }, + "---" + }; + Text text_slices { + { 8 * 8, 3 * 16, 2 * 8, 16 }, + "--" + }; + Text text_rate { + { 24 * 8, 3 * 16, 3 * 8, 16 }, + "---" + }; + + VuMeter vu_max { + { 1 * 8, 11 * 8 - 4, 3 * 8, 48 }, + 16, + false + }; + ProgressBar progress_timers { + { 6 * 8, 12 * 8, 5 * 8, 16 } + }; + Text text_infos { + { 13 * 8, 12 * 8, 15 * 8, 16 }, + "Listening" + }; + Checkbox check_goto { + { 6 * 8, 15 * 8 }, + 8, + "On lock:", + true + }; + OptionsField options_goto { + { 17 * 8, 15 * 8 }, + 7, + { + { "Nothing", 0 }, + { "NFM RX ", 1 }, + { "POCSAG ", 2 } + } + }; + + BigFrequency big_display { + { 4, 9 * 16, 28 * 8, 52 }, + 0 + }; + + Text text_approx { + { 11 * 8, 25 * 8, 11 * 8, 16 }, + "..." + }; + + MessageHandlerRegistration message_handler_spectrum_config { + Message::ID::ChannelSpectrumConfig, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->fifo = message.fifo; + } + }; + MessageHandlerRegistration message_handler_frame_sync { + Message::ID::DisplayFrameSync, + [this](const Message* const) { + if( this->fifo ) { + ChannelSpectrum channel_spectrum; + while( fifo->out(channel_spectrum) ) { + this->on_channel_spectrum(channel_spectrum); + } + } + this->do_timers(); + } + }; +}; + +} /* namespace ui */ diff --git a/firmware/baseband/audio_output.cpp b/firmware/baseband/audio_output.cpp index b52dc312..e2bc851e 100644 --- a/firmware/baseband/audio_output.cpp +++ b/firmware/baseband/audio_output.cpp @@ -76,8 +76,6 @@ void AudioOutput::write( void AudioOutput::on_block( const buffer_f32_t& audio ) { - bool audio_present; - if (do_processing) { const auto audio_present_now = squelch.execute(audio); @@ -98,6 +96,10 @@ void AudioOutput::on_block( fill_audio_buffer(audio, audio_present); } +bool AudioOutput::is_squelched() { + return ~audio_present; +} + void AudioOutput::fill_audio_buffer(const buffer_f32_t& audio, const bool send_to_fifo) { std::array audio_int; diff --git a/firmware/baseband/audio_output.hpp b/firmware/baseband/audio_output.hpp index 0eb1d961..517f6340 100644 --- a/firmware/baseband/audio_output.hpp +++ b/firmware/baseband/audio_output.hpp @@ -51,6 +51,8 @@ public: void set_stream(std::unique_ptr new_stream) { stream = std::move(new_stream); } + + bool is_squelched(); private: static constexpr float k = 32768.0f; @@ -68,6 +70,7 @@ private: uint64_t audio_present_history = 0; + bool audio_present = false; bool do_processing = true; void on_block(const buffer_f32_t& audio); diff --git a/firmware/baseband/proc_nfm_audio.cpp b/firmware/baseband/proc_nfm_audio.cpp index fc0073c1..b25b13a8 100644 --- a/firmware/baseband/proc_nfm_audio.cpp +++ b/firmware/baseband/proc_nfm_audio.cpp @@ -21,6 +21,7 @@ */ #include "proc_nfm_audio.hpp" +#include "portapack_shared_memory.hpp" #include "event_m4.hpp" @@ -28,6 +29,8 @@ #include void NarrowbandFMAudio::execute(const buffer_c8_t& buffer) { + bool new_state; + if( !configured ) { return; } @@ -56,6 +59,13 @@ void NarrowbandFMAudio::execute(const buffer_c8_t& buffer) { } audio_output.write(pwmrssi_audio_buffer); + + new_state = audio_output.is_squelched(); + + if (new_state && !old_state) + shared_memory.application_queue.push(sig_message); + + old_state = new_state; } } @@ -102,7 +112,8 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) { channel_filter_pass_f = message.channel_filter.pass_frequency_normalized * channel_filter_input_fs; channel_filter_stop_f = message.channel_filter.stop_frequency_normalized * channel_filter_input_fs; channel_spectrum.set_decimation_factor(std::floor(channel_filter_output_fs / (channel_filter_pass_f + channel_filter_stop_f))); - audio_output.configure(message.audio_hpf_config, message.audio_deemph_config); // , 0.8f + // TODO: Configurable squelch threshold + audio_output.configure(message.audio_hpf_config, message.audio_deemph_config, 0.8f); synth_acc = 0; diff --git a/firmware/baseband/proc_nfm_audio.hpp b/firmware/baseband/proc_nfm_audio.hpp index 1f2a891a..92d9d4f7 100644 --- a/firmware/baseband/proc_nfm_audio.hpp +++ b/firmware/baseband/proc_nfm_audio.hpp @@ -73,6 +73,7 @@ private: dsp::demodulate::FM demod { }; AudioOutput audio_output { }; + bool old_state { }; SpectrumCollector channel_spectrum { }; @@ -85,6 +86,8 @@ private: void pwmrssi_config(const PWMRSSIConfigureMessage& message); void configure(const NBFMConfigureMessage& message); void capture_config(const CaptureConfigMessage& message); + + RequestSignalMessage sig_message { RequestSignalMessage::Signal::Squelched }; }; #endif/*__PROC_NFM_AUDIO_H__*/ diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index a04ed06f..c1988d5a 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -821,11 +821,13 @@ public: }; // TODO: use streaming buffer instead +// TODO: rename (not only used for requests) class RequestSignalMessage : public Message { public: enum class Signal : char { FillRequest = 1, BeepRequest = 2, + Squelched = 3 }; constexpr RequestSignalMessage( diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 3bffd12f..1fe80a50 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -457,12 +457,16 @@ ProgressBar::ProgressBar( } void ProgressBar::set_max(const uint32_t max) { + if (max == _max) return; + _value = 0; _max = max; set_dirty(); } void ProgressBar::set_value(const uint32_t value) { + if (value == _value) return; + if (value > _max) _value = _max; else @@ -1450,37 +1454,45 @@ void Waveform::paint(Painter& painter) { VuMeter::VuMeter( Rect parent_rect, - uint32_t LEDs + uint32_t LEDs, + bool show_max ) : Widget { parent_rect }, LEDs_ { LEDs }, - height { parent_rect.size().height() } + show_max_ { show_max } { //set_focusable(false); - LED_height = height / LEDs; + LED_height = parent_rect.size().height() / LEDs; split = 256 / LEDs; } -void VuMeter::set_value(const uint8_t new_value) { - value_ = new_value; - set_dirty(); +void VuMeter::set_value(const uint32_t new_value) { + if ((new_value != value_) && (new_value < 256)) { + value_ = new_value; + set_dirty(); + } } -void VuMeter::set_mark(const uint8_t new_mark) { - if (new_mark != mark) { +void VuMeter::set_mark(const uint32_t new_mark) { + if ((new_mark != mark) && (new_mark < 256)) { mark = new_mark; set_dirty(); } } void VuMeter::paint(Painter& painter) { + uint32_t bar; + Color color; + bool lit = false; + uint32_t bar_level; Point pos = screen_rect().location(); - Dim width = screen_rect().size().width() - 4; + Dim width = screen_rect().size().width() - 4; + Dim height = screen_rect().size().height(); + Dim bottom = pos.y() + height; + Coord marks_x = pos.x() + width; if (value_ != prev_value) { - uint32_t bar; - Color color; - bool lit = false; - uint32_t bar_level = LEDs_ - ((value_ + 1) / split); + bar_level = LEDs_ - ((value_ + 1) / split); + // Draw LEDs for (bar = 0; bar < LEDs_; bar++) { if (bar >= bar_level) @@ -1501,33 +1513,34 @@ void VuMeter::paint(Painter& painter) { } // Update max level - if (value_ > max) { - max = value_; - hold_timer = 30; // 0.5s @ 60Hz - } else { - if (hold_timer) { - hold_timer--; + if (show_max_) { + if (value_ > max) { + max = value_; + hold_timer = 30; // 0.5s @ 60Hz } else { - if (max) max--; // Let it drop + if (hold_timer) { + hold_timer--; + } else { + if (max) max--; // Let it drop + } + } + + // Draw max level + if (max != prev_max) { + painter.draw_hline({ marks_x, bottom - (height * prev_max) / 256 }, 8, Color::black()); + painter.draw_hline({ marks_x, bottom - (height * max) / 256 }, 8, Color::white()); + if (prev_max == mark) + prev_mark = 0; // Force mark refresh + prev_max = max; } - } - - // Draw max level - if (max != prev_max) { - painter.draw_hline({ pos.x() + width, pos.y() + height - (height * prev_max) / 256 }, 8, Color::black()); - painter.draw_hline({ pos.x() + width, pos.y() + height - (height * max) / 256 }, 8, Color::white()); - if (prev_max == mark) - prev_mark = 0; // Force mark refresh } // Draw mark if ((mark != prev_mark) && (mark)) { - painter.draw_hline({ pos.x() + width, pos.y() + height - (height * prev_mark) / 256 }, 8, Color::black()); - painter.draw_hline({ pos.x() + width, pos.y() + height - (height * mark) / 256 }, 8, Color::grey()); + painter.draw_hline({ marks_x, bottom - (height * prev_mark) / 256 }, 8, Color::black()); + painter.draw_hline({ marks_x, bottom - (height * mark) / 256 }, 8, Color::grey()); + prev_mark = mark; } - - prev_max = max; - prev_mark = mark; } } /* namespace ui */ diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 1ff58c27..7f60f238 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -575,19 +575,19 @@ private: class VuMeter : public Widget { public: - VuMeter(Rect parent_rect, uint32_t LEDs); + VuMeter(Rect parent_rect, uint32_t LEDs, bool show_max); - void set_value(const uint8_t new_value); - void set_mark(const uint8_t new_mark); + void set_value(const uint32_t new_value); + void set_mark(const uint32_t new_mark); void paint(Painter& painter) override; private: uint32_t LEDs_, LED_height { 0 }; - uint32_t value_ { 0 }, prev_value { 0 }; + uint32_t value_ { 0 }, prev_value { 255 }; // Forces painting on first display uint32_t split { 0 }; uint16_t max { 0 }, prev_max { 0 }, hold_timer { 0 }, mark { 0 }, prev_mark { 0 }; - int height; + bool show_max_; }; } /* namespace ui */ diff --git a/firmware/portapack-h1-havoc.bin b/firmware/portapack-h1-havoc.bin index 62261212..735e8f7f 100644 Binary files a/firmware/portapack-h1-havoc.bin and b/firmware/portapack-h1-havoc.bin differ