mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2025-08-14 12:48:12 +00:00
Externalize scanner (#2589)
* externalize scanner * NFM as main baseband as it's the biggest used one * fix modulation bug introduced with AMFM
This commit is contained in:
5
firmware/application/external/external.cmake
vendored
5
firmware/application/external/external.cmake
vendored
@@ -192,6 +192,10 @@ set(EXTCPPSRC
|
||||
external/doom/main.cpp
|
||||
external/doom/ui_doom.cpp
|
||||
|
||||
#scanner
|
||||
external/scanner/main.cpp
|
||||
external/scanner/ui_scanner.cpp
|
||||
|
||||
#debug_pmem
|
||||
external/debug_pmem/main.cpp
|
||||
external/debug_pmem/ui_debug_pmem.cpp
|
||||
@@ -246,4 +250,5 @@ set(EXTAPPLIST
|
||||
breakout
|
||||
doom
|
||||
debug_pmem
|
||||
scanner
|
||||
)
|
||||
|
8
firmware/application/external/external.ld
vendored
8
firmware/application/external/external.ld
vendored
@@ -70,6 +70,7 @@ MEMORY
|
||||
ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k
|
||||
ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k
|
||||
ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k
|
||||
ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -354,4 +355,11 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_debug_pmem.application_information));
|
||||
*(*ui*external_app*debug_pmem*);
|
||||
} > ram_external_app_debug_pmem
|
||||
|
||||
.external_app_scanner : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_scanner.application_information));
|
||||
*(*ui*external_app*scanner*);
|
||||
} > ram_external_app_scanner
|
||||
|
||||
}
|
||||
|
51
firmware/application/external/scanner/main.cpp
vendored
Normal file
51
firmware/application/external/scanner/main.cpp
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_scanner.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::scanner {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ScannerView>();
|
||||
}
|
||||
} // namespace ui::external_app::scanner
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_scanner.application_information"), used)) application_information_t _application_information_scanner = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::scanner::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Scanner",
|
||||
/*.bitmap_data = */ {0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x03, 0x01, 0x80, 0x01, 0xC3, 0x00, 0xE0, 0xFF, 0xEF, 0xFF, 0xC0, 0x00, 0x83, 0x01, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
// this has to be the biggest baseband used by the app. Scanner is using AM,WFM,NFM and NFM is the biggest
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_scanner */ {'P', 'N', 'F', 'M'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
778
firmware/application/external/scanner/ui_scanner.cpp
vendored
Normal file
778
firmware/application/external/scanner/ui_scanner.cpp
vendored
Normal file
@@ -0,0 +1,778 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_scanner.hpp"
|
||||
|
||||
#include "optional.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace ui;
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui::external_app::scanner {
|
||||
|
||||
ScannerThread::ScannerThread(std::vector<rf::Frequency> frequency_list)
|
||||
: frequency_list_{std::move(frequency_list)} {
|
||||
_manual_search = false;
|
||||
create_thread();
|
||||
}
|
||||
|
||||
ScannerThread::ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz)
|
||||
: frequency_range_(frequency_range), def_step_hz_(def_step_hz) {
|
||||
_manual_search = true;
|
||||
create_thread();
|
||||
}
|
||||
|
||||
ScannerThread::~ScannerThread() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void ScannerThread::create_thread() {
|
||||
thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 10, ScannerThread::static_fn, this);
|
||||
}
|
||||
|
||||
void ScannerThread::stop() {
|
||||
if (thread) {
|
||||
chThdTerminate(thread);
|
||||
chThdWait(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Set by "userpause"
|
||||
void ScannerThread::set_scanning(const bool v) {
|
||||
_scanning = v;
|
||||
}
|
||||
|
||||
bool ScannerThread::is_scanning() {
|
||||
return _scanning;
|
||||
}
|
||||
|
||||
void ScannerThread::set_freq_lock(const uint32_t v) {
|
||||
_freq_lock = v;
|
||||
}
|
||||
|
||||
uint32_t ScannerThread::is_freq_lock() {
|
||||
return _freq_lock;
|
||||
}
|
||||
|
||||
// Delete an entry from frequency list
|
||||
// Caller must pause scan_thread AND can't delete a second one until this field is cleared
|
||||
void ScannerThread::set_freq_del(const rf::Frequency v) {
|
||||
_freq_del = v;
|
||||
}
|
||||
|
||||
// Force a one-time forward or reverse frequency index change; OK to do this without pausing scan thread
|
||||
// (used when rotary encoder is turned)
|
||||
void ScannerThread::set_index_stepper(const int32_t v) {
|
||||
_index_stepper = v;
|
||||
}
|
||||
|
||||
// Set scanning direction; OK to do this without pausing scan_thread
|
||||
void ScannerThread::set_scanning_direction(bool fwd) {
|
||||
int32_t new_stepper = fwd ? 1 : -1;
|
||||
|
||||
if (_stepper != new_stepper) {
|
||||
_stepper = new_stepper;
|
||||
chThdSleepMilliseconds(300); // Give some pause after reversing scanning direction
|
||||
}
|
||||
}
|
||||
|
||||
msg_t ScannerThread::static_fn(void* arg) {
|
||||
auto obj = static_cast<ScannerThread*>(arg);
|
||||
obj->run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ScannerThread::run() {
|
||||
RetuneMessage message{};
|
||||
|
||||
if (!_manual_search && frequency_list_.size()) { // IF NOT MANUAL MODE AND THERE IS A FREQUENCY LIST ...
|
||||
int32_t size = frequency_list_.size();
|
||||
int32_t frequency_index = (_stepper > 0) ? size : 0; // Forcing wraparound to starting frequency on 1st pass
|
||||
|
||||
while (!chThdShouldTerminate()) {
|
||||
bool force_one_step = (_index_stepper != 0);
|
||||
int32_t step = force_one_step ? _index_stepper : _stepper; //_index_stepper direction takes priority
|
||||
|
||||
if (_scanning || force_one_step) { // Scanning, or paused and using rotary encoder
|
||||
if ((_freq_lock == 0) || force_one_step) { // normal scanning (not performing freq_lock)
|
||||
frequency_index += step;
|
||||
if (frequency_index >= size) // Wrap
|
||||
frequency_index = 0;
|
||||
else if (frequency_index < 0)
|
||||
frequency_index = size - 1;
|
||||
|
||||
if (force_one_step)
|
||||
_index_stepper = 0;
|
||||
|
||||
receiver_model.set_target_frequency(frequency_list_[frequency_index]); // Retune
|
||||
}
|
||||
message.freq = frequency_list_[frequency_index];
|
||||
message.range = frequency_index; // Inform freq (for coloring purposes also!)
|
||||
EventDispatcher::send_message(message);
|
||||
} else if (_freq_del != 0) { // There is a frequency to delete
|
||||
for (int32_t i = 0; i < size; i++) { // Search for the freq to delete
|
||||
if (frequency_list_[i] == _freq_del) { // found: Erase it
|
||||
frequency_list_.erase(frequency_list_.begin() + i);
|
||||
size = frequency_list_.size();
|
||||
break;
|
||||
}
|
||||
}
|
||||
_freq_del = 0; // deleted.
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(SCANNER_SLEEP_MS); // Needed to (eventually) stabilize the receiver into new freq
|
||||
}
|
||||
} else if (_manual_search && (def_step_hz_ > 0)) // manual search range mode
|
||||
{
|
||||
int64_t size = (frequency_range_.max - frequency_range_.min) / def_step_hz_;
|
||||
int64_t frequency_index = (_stepper > 0) ? size : 0; // Forcing wraparound to starting frequency on 1st pass
|
||||
|
||||
while (!chThdShouldTerminate()) {
|
||||
bool force_one_step = (_index_stepper != 0);
|
||||
int32_t step = force_one_step ? _index_stepper : _stepper; //_index_stepper direction takes priority
|
||||
|
||||
if (_scanning || force_one_step) { // Scanning, or paused and using rotary encoder
|
||||
if ((_freq_lock == 0) || force_one_step) { // normal scanning (not performing freq_lock)
|
||||
frequency_index += step;
|
||||
if (frequency_index >= size) // Wrap
|
||||
frequency_index = 0;
|
||||
else if (frequency_index < 0)
|
||||
frequency_index = size - 1;
|
||||
|
||||
if (force_one_step)
|
||||
_index_stepper = 0;
|
||||
|
||||
receiver_model.set_target_frequency(frequency_range_.min + frequency_index * def_step_hz_); // Retune
|
||||
}
|
||||
message.freq = frequency_range_.min + frequency_index * def_step_hz_;
|
||||
message.range = 0; // Inform freq (for coloring purposes also!)
|
||||
EventDispatcher::send_message(message);
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(SCANNER_SLEEP_MS); // Needed to (eventually) stabilize the receiver into new freq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::bigdisplay_update(int32_t v) {
|
||||
if (v != bigdisplay_current_color) {
|
||||
if (v != -1)
|
||||
bigdisplay_current_color = v; // -1 means refresh display but keep current color
|
||||
|
||||
switch (bigdisplay_current_color) {
|
||||
case BDC_GREY:
|
||||
big_display.set_style(Theme::getInstance()->fg_medium);
|
||||
break;
|
||||
case BDC_YELLOW:
|
||||
big_display.set_style(Theme::getInstance()->fg_yellow);
|
||||
break;
|
||||
case BDC_GREEN:
|
||||
big_display.set_style(Theme::getInstance()->fg_green);
|
||||
break;
|
||||
case BDC_RED:
|
||||
big_display.set_style(Theme::getInstance()->fg_red);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// update frequency display
|
||||
bigdisplay_current_frequency = current_frequency;
|
||||
big_display.set(bigdisplay_current_frequency);
|
||||
} else {
|
||||
// no style change, but update frequency display if it's changed
|
||||
if (current_frequency != bigdisplay_current_frequency) {
|
||||
bigdisplay_current_frequency = current_frequency;
|
||||
big_display.set(bigdisplay_current_frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::handle_retune(int64_t freq, uint32_t freq_idx) {
|
||||
current_index = freq_idx; // since it is an ongoing scan, this is a new index
|
||||
current_frequency = freq;
|
||||
|
||||
if (scan_thread) {
|
||||
switch (scan_thread->is_freq_lock()) {
|
||||
case 0: // NO FREQ LOCK, ONGOING STANDARD SCANNING
|
||||
bigdisplay_update(BDC_GREY);
|
||||
break;
|
||||
case 1: // STARTING LOCK FREQ
|
||||
bigdisplay_update(BDC_YELLOW);
|
||||
break;
|
||||
case MAX_FREQ_LOCK: // FREQ IS STRONG: GREEN and scanner will pause when on_statistics_update()
|
||||
bigdisplay_update(BDC_GREEN);
|
||||
break;
|
||||
default: // freq lock is checking the signal, do not update display
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manual_search) {
|
||||
if (entries.size() > 0)
|
||||
field_current_index.set_text(to_string_dec_uint(freq_idx + 1, 3));
|
||||
|
||||
if (freq_idx < entries.size() && entries[freq_idx].description.size() > 1)
|
||||
text_current_desc.set(entries[freq_idx].description); // Show description from file
|
||||
else
|
||||
text_current_desc.set(loaded_filename()); // Show Scan file name (no description in file)
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::handle_encoder(EncoderEvent delta) {
|
||||
auto index_step = delta > 0 ? 1 : -1;
|
||||
|
||||
if (scan_thread)
|
||||
scan_thread->set_index_stepper(index_step);
|
||||
|
||||
// Restart browse timer when frequency changes.
|
||||
if (browse_timer != 0)
|
||||
browse_timer = 1;
|
||||
}
|
||||
|
||||
std::string ScannerView::loaded_filename() const {
|
||||
auto filename = freqman_file;
|
||||
if (filename.length() > 23) { // Truncate long file name.
|
||||
filename.resize(22);
|
||||
filename = filename + "+";
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
void ScannerView::focus() {
|
||||
button_load.focus();
|
||||
}
|
||||
|
||||
ScannerView::~ScannerView() {
|
||||
// make sure to stop the thread before shutting down the receiver
|
||||
scan_thread.reset();
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ScannerView::show_max_index() { // show total number of freqs to scan
|
||||
field_current_index.set_text("<->");
|
||||
|
||||
if (entries.size() == FREQMAN_MAX_PER_FILE) {
|
||||
text_max_index.set_style(Theme::getInstance()->fg_red);
|
||||
text_max_index.set("/ " + to_string_dec_uint(FREQMAN_MAX_PER_FILE) + " (DB MAX!)");
|
||||
} else {
|
||||
text_max_index.set_style(Theme::getInstance()->fg_medium);
|
||||
text_max_index.set("/ " + to_string_dec_uint(entries.size()));
|
||||
}
|
||||
}
|
||||
|
||||
ScannerView::ScannerView(
|
||||
NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({
|
||||
&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&field_volume,
|
||||
&field_bw,
|
||||
&field_squelch,
|
||||
&field_browse_wait,
|
||||
&field_lock_wait,
|
||||
&button_load,
|
||||
&button_clear,
|
||||
&rssi,
|
||||
&field_current_index,
|
||||
&text_max_index,
|
||||
&text_current_desc,
|
||||
&big_display,
|
||||
&button_manual_start,
|
||||
&button_manual_end,
|
||||
&field_mode,
|
||||
&field_step,
|
||||
&button_manual_search,
|
||||
&button_pause,
|
||||
&button_dir,
|
||||
&button_audio_app,
|
||||
&button_mic_app,
|
||||
&button_add,
|
||||
&button_remove,
|
||||
});
|
||||
|
||||
// Populate option text for these fields
|
||||
freqman_set_modulation_option(field_mode);
|
||||
freqman_set_step_option(field_step);
|
||||
|
||||
// Default starting modulation (from saved App Settings if enabled, and may be overridden in SCANNER.TXT)
|
||||
field_mode.set_by_value((OptionsField::value_t)receiver_model.modulation()); // Reflect the mode into the manual selector
|
||||
field_step.set_by_value(receiver_model.frequency_step()); // Default step interval (Hz)
|
||||
change_mode((freqman_index_t)field_mode.selected_index_value());
|
||||
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
|
||||
// Button to load a Freqman file.
|
||||
button_load.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
if (new_file_path.native().find((u"/" / freqman_dir).native()) == 0) {
|
||||
scan_pause();
|
||||
frequency_file_load(new_file_path);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Button to clear in-memory frequency list.
|
||||
button_clear.on_select = [this, &nav](Button&) {
|
||||
if (scan_thread && entries.size()) {
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
entries.clear();
|
||||
|
||||
show_max_index(); // UPDATE new list size on screen
|
||||
field_current_index.set_text("");
|
||||
text_current_desc.set(loaded_filename());
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock
|
||||
|
||||
// FUTURE: Consider switching to manual search mode automatically after clear (but would need to validate freq range)
|
||||
}
|
||||
};
|
||||
|
||||
// Button to configure starting frequency for a manual range search.
|
||||
button_manual_start.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(frequency_range.min);
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
frequency_range.min = f;
|
||||
button_manual_start.set_text(to_string_short_freq(f));
|
||||
};
|
||||
};
|
||||
|
||||
// Button to configure ending frequency for a manual range search.
|
||||
button_manual_end.on_select = [this, &nav](Button& button) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.max);
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
frequency_range.max = f;
|
||||
button_manual_end.set_text(to_string_short_freq(f));
|
||||
};
|
||||
};
|
||||
|
||||
// Button to pause/resume scan (note that some other buttons will trigger resume also).
|
||||
button_pause.on_select = [this](ButtonWithEncoder&) {
|
||||
if (userpause)
|
||||
user_resume();
|
||||
else {
|
||||
scan_pause();
|
||||
button_pause.set_text("<RESUME>"); // PAUSED, show resume
|
||||
userpause = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Encoder dial causes frequency change when focus is on pause button or current index.
|
||||
button_pause.on_change = [this]() {
|
||||
handle_encoder(button_pause.get_encoder_delta());
|
||||
button_pause.set_encoder_delta(0);
|
||||
};
|
||||
field_current_index.on_encoder_change = [this](TextField&, EncoderEvent delta) {
|
||||
handle_encoder(delta);
|
||||
};
|
||||
|
||||
// Button to switch to Audio app
|
||||
button_audio_app.on_select = [this](Button&) {
|
||||
if (scan_thread)
|
||||
scan_thread->stop();
|
||||
auto settings = receiver_model.settings();
|
||||
settings.frequency_step = field_step.selected_index_value();
|
||||
nav_.replace<AnalogAudioView>(settings);
|
||||
};
|
||||
|
||||
// Button to switch to Mic app
|
||||
button_mic_app.on_select = [this](Button&) {
|
||||
if (scan_thread)
|
||||
scan_thread->stop();
|
||||
// MicTX wants Frequency, Modulation and Bandwidth overrides, but that's only stored on the RX model.
|
||||
nav_.replace<MicTXView>(receiver_model.settings());
|
||||
};
|
||||
|
||||
// Button to delete current frequency from scan Freq List
|
||||
button_remove.on_select = [this](Button&) {
|
||||
if (scan_thread && (entries.size() > current_index)) {
|
||||
scan_thread->set_scanning(false); // PAUSE Scanning if necessary
|
||||
|
||||
// Remove frequency from the Freq List in memory (it is not removed from the file).
|
||||
scan_thread->set_freq_del(entries[current_index].freq);
|
||||
entries.erase(entries.begin() + current_index);
|
||||
|
||||
show_max_index(); // UPDATE new list size on screen
|
||||
text_current_desc.set(""); // Clean up description (cosmetic detail)
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock
|
||||
}
|
||||
};
|
||||
|
||||
// Button to toggle between Manual Search and Freq List Scan modes
|
||||
button_manual_search.on_select = [this](Button&) {
|
||||
if (!manual_search) {
|
||||
if (!frequency_range.min || !frequency_range.max) {
|
||||
nav_.display_modal("Error", "Both START and END freqs\nneed a value");
|
||||
} else if (frequency_range.min > frequency_range.max) {
|
||||
nav_.display_modal("Error", "END freq\nis lower than START");
|
||||
} else {
|
||||
manual_search = true; // Switch to Manual Search mode
|
||||
}
|
||||
} else {
|
||||
manual_search = false; // Switch to List Scan mode
|
||||
}
|
||||
|
||||
restart_scan();
|
||||
};
|
||||
|
||||
// Mode field was changed (AM/NFM/WFM)
|
||||
field_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
// initialize to a value under SPEC
|
||||
static freqman_index_t last_mode = WFM_MODULATION;
|
||||
// unsupported SPEC mode fix
|
||||
if (v == SPEC_MODULATION) {
|
||||
if (last_mode == WFM_MODULATION)
|
||||
v = AMFM_MODULATION;
|
||||
else
|
||||
v = WFM_MODULATION;
|
||||
field_mode.set_selected_index(v);
|
||||
}
|
||||
last_mode = v;
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
change_mode((freqman_index_t)v);
|
||||
if (scan_thread && !scan_thread->is_scanning()) // for some motive, audio output gets stopped.
|
||||
audio::output::start(); // So if scan was stopped we resume audio
|
||||
receiver_model.enable();
|
||||
};
|
||||
|
||||
// Step field was changed (Hz) -- only affects manual Search mode
|
||||
field_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
|
||||
if (manual_search && scan_thread)
|
||||
restart_scan();
|
||||
};
|
||||
|
||||
// Button to toggle Forward/Reverse
|
||||
button_dir.on_select = [this](Button&) {
|
||||
fwd = !fwd;
|
||||
if (scan_thread)
|
||||
scan_thread->set_scanning_direction(fwd);
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
button_dir.set_text(fwd ? "REVERSE" : "FORWARD");
|
||||
bigdisplay_update(BDC_GREY); // Back to grey color
|
||||
};
|
||||
|
||||
// Button to add current frequency (found during Search) to the Scan Frequency List
|
||||
button_add.on_select = [this](Button&) {
|
||||
FreqmanDB db;
|
||||
if (db.open(get_freqman_path(freqman_file), /*create*/ true)) {
|
||||
freqman_entry entry{
|
||||
.frequency_a = current_frequency,
|
||||
.type = freqman_type::Single,
|
||||
};
|
||||
|
||||
// Look for existing entry with same frequency.
|
||||
auto it = db.find_entry([&entry](const auto& e) {
|
||||
return e.frequency_a == entry.frequency_a;
|
||||
});
|
||||
auto found = (it != db.end());
|
||||
|
||||
if (found) {
|
||||
nav_.display_modal("Error", "Frequency already exists");
|
||||
bigdisplay_update(-1); // Need to poke this control after displaying modal?
|
||||
} else {
|
||||
db.append_entry(entry);
|
||||
// Add to frequency_list in memory too, since we can now switch back from manual mode
|
||||
// Note that we are allowing freqs to be added to file (code above) that exceed the
|
||||
// max count we can load into memory.
|
||||
if (entries.size() < FREQMAN_MAX_PER_FILE) {
|
||||
entries.push_back({current_frequency, ""});
|
||||
show_max_index(); // Display updated frequency list size
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nav_.display_modal("Error", "Cannot open " + freqman_file + ".TXT\nfor appending freq.");
|
||||
bigdisplay_update(-1); // Need to poke this control after displaying modal?
|
||||
}
|
||||
};
|
||||
|
||||
// PRE-CONFIGURATION:
|
||||
field_browse_wait.on_change = [this](int32_t v) { browse_wait = v; };
|
||||
field_browse_wait.set_value(browse_wait);
|
||||
|
||||
field_lock_wait.on_change = [this](int32_t v) { lock_wait = v; };
|
||||
field_lock_wait.set_value(lock_wait);
|
||||
|
||||
field_squelch.on_change = [this](int32_t v) { squelch = v; };
|
||||
field_squelch.set_value(squelch);
|
||||
|
||||
// Disable squelch on the model because RSSI handler is where the
|
||||
// actual squelching is applied for this app.
|
||||
receiver_model.set_squelch_level(0);
|
||||
|
||||
// LOAD FREQUENCIES
|
||||
frequency_file_load(get_freqman_path(freqman_file));
|
||||
}
|
||||
|
||||
void ScannerView::frequency_file_load(const fs::path& path) {
|
||||
freqman_index_t def_mod_index{freqman_invalid_index};
|
||||
freqman_index_t def_bw_index{freqman_invalid_index};
|
||||
freqman_index_t def_step_index{freqman_invalid_index};
|
||||
|
||||
FreqmanDB db;
|
||||
if (!db.open(path)) {
|
||||
text_current_desc.set("NO " + path.filename().string());
|
||||
return;
|
||||
}
|
||||
|
||||
entries.clear();
|
||||
freqman_file = path.stem().string();
|
||||
Optional<scanner_range_t> range;
|
||||
|
||||
for (auto entry : db) {
|
||||
if (is_invalid(def_mod_index))
|
||||
def_mod_index = entry.modulation;
|
||||
|
||||
if (is_invalid(def_bw_index))
|
||||
def_bw_index = entry.bandwidth;
|
||||
|
||||
if (is_invalid(def_step_index))
|
||||
def_step_index = entry.step;
|
||||
|
||||
switch (entry.type) {
|
||||
case freqman_type::Repeater:
|
||||
case freqman_type::Single:
|
||||
entries.push_back({entry.frequency_a, entry.description});
|
||||
break;
|
||||
case freqman_type::HamRadio:
|
||||
entries.push_back({entry.frequency_a, "R: " + entry.description});
|
||||
entries.push_back({entry.frequency_b, "T: " + entry.description});
|
||||
break;
|
||||
case freqman_type::Range:
|
||||
// NB: Only the first range will be loaded.
|
||||
if (!range)
|
||||
range = {entry.frequency_a, entry.frequency_b};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (entries.size() >= FREQMAN_MAX_PER_FILE)
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_valid(def_mod_index) && def_mod_index != (freqman_index_t)field_mode.selected_index_value())
|
||||
field_mode.set_by_value(def_mod_index);
|
||||
|
||||
if (is_valid(def_bw_index))
|
||||
field_bw.set_selected_index(def_bw_index);
|
||||
|
||||
if (is_valid(def_step_index))
|
||||
field_step.set_selected_index(def_step_index);
|
||||
|
||||
// Found range, set it and update UI.
|
||||
if (range) {
|
||||
frequency_range = *range;
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
}
|
||||
|
||||
// Scan entries if any, otherwise do manual range search.
|
||||
manual_search = entries.empty();
|
||||
restart_scan();
|
||||
}
|
||||
|
||||
void ScannerView::update_squelch_while_paused(int32_t max_db) {
|
||||
// Update audio & color based on signal level even if paused
|
||||
if (++color_timer > 2) { // Counter to reduce color toggling when weak signal
|
||||
if (max_db > squelch) {
|
||||
audio::output::start(); // Re-enable audio when signal goes above squelch
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // quick fix to make sure WM8731S chips don't stay silent after pause
|
||||
bigdisplay_update(BDC_GREEN);
|
||||
} else {
|
||||
audio::output::stop(); // Silence audio when signal drops below squelch
|
||||
bigdisplay_update(BDC_GREY); // Back to grey color
|
||||
}
|
||||
|
||||
color_timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
if (userpause) {
|
||||
update_squelch_while_paused(statistics.max_db);
|
||||
} else if (scan_thread) // Scanning not user-paused
|
||||
{
|
||||
// Resume regardless of signal strength if browse time reached
|
||||
if ((browse_wait != 0) && (browse_timer >= (browse_wait * STATISTICS_UPDATES_PER_SEC))) {
|
||||
browse_timer = 0;
|
||||
scan_resume(); // Resume scanning
|
||||
} else {
|
||||
if (statistics.max_db > squelch) { // There is something on the air...(statistics.max_db > -squelch)
|
||||
if (scan_thread->is_freq_lock() >= MAX_FREQ_LOCK) { // Pause scanning when signal checking time reached
|
||||
if (!browse_timer) // Don't bother pausing if already paused
|
||||
scan_pause();
|
||||
browse_timer++; // browse_timer!=0 is also an indication that we've paused the scan
|
||||
update_squelch_while_paused(statistics.max_db);
|
||||
} else {
|
||||
scan_thread->set_freq_lock(scan_thread->is_freq_lock() + 1); // in lock period, still analyzing the signal
|
||||
if (browse_timer) // Continue incrementing browse timer while paused
|
||||
browse_timer++;
|
||||
}
|
||||
lock_timer = 0; // Keep resetting lock timer while signal remains
|
||||
} else { // There is NOTHING on the air
|
||||
if (!browse_timer) {
|
||||
// Signal lost and scan was never paused
|
||||
if (scan_thread->is_freq_lock() > 0) { // But are we already in freq_lock ?
|
||||
bigdisplay_update(BDC_GREY); // Back to grey color
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock, since there is no sig
|
||||
}
|
||||
} else {
|
||||
// Signal lost and scan is still paused
|
||||
lock_timer++; // Bump paused time
|
||||
if (lock_timer >= (lock_wait * STATISTICS_UPDATES_PER_SEC)) { // Stay on freq until lock_wait time elapses
|
||||
browse_timer = 0;
|
||||
scan_resume();
|
||||
} else {
|
||||
browse_timer++; // Bump browse time too (may hit that limit before lock_timer reached)
|
||||
update_squelch_while_paused(statistics.max_db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::scan_pause() {
|
||||
if (scan_thread && scan_thread->is_scanning()) {
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock (because user paused, or MAX_FREQ_LOCK reached) for next freq scan
|
||||
scan_thread->set_scanning(false); // WE STOP SCANNING
|
||||
}
|
||||
audio::output::start();
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // quick fix to make sure WM8731S chips don't stay silent after pause
|
||||
}
|
||||
|
||||
void ScannerView::scan_resume() {
|
||||
audio::output::stop();
|
||||
bigdisplay_update(BDC_GREY); // Back to grey color
|
||||
|
||||
if (scan_thread) {
|
||||
scan_thread->set_index_stepper(fwd ? 1 : -1);
|
||||
scan_thread->set_scanning(true); // RESUME!
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::user_resume() {
|
||||
browse_timer = browse_wait * STATISTICS_UPDATES_PER_SEC + 1; // Will trigger a scan_resume() on_statistics_update, also advancing to next freq.
|
||||
button_pause.set_text("<PAUSE>"); // Show button for pause, arrows indicate rotary encoder enabled for freq change
|
||||
userpause = false; // Resume scanning
|
||||
}
|
||||
|
||||
// Before this, do a scan_thread->stop(); After this do a start_scan_thread()
|
||||
void ScannerView::change_mode(freqman_index_t new_mod) {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t bw;
|
||||
field_bw.on_change = [this](size_t n, OptionsField::value_t) {
|
||||
(void)n; // avoid unused warning
|
||||
};
|
||||
|
||||
switch (new_mod) {
|
||||
case AM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
field_bw.set_by_value(receiver_model.am_configuration());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration(n); };
|
||||
break;
|
||||
case NFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
field_bw.set_by_value(receiver_model.nbfm_configuration());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_nbfm_configuration(n); };
|
||||
break;
|
||||
case WFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
field_bw.set_by_value(receiver_model.wfm_configuration());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_wfm_configuration(n); };
|
||||
break;
|
||||
case AMFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudioFMApt);
|
||||
receiver_model.set_amfm_configuration(5);
|
||||
field_bw.set_by_value(0);
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { (void)n; };
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerView::start_scan_thread() {
|
||||
receiver_model.enable();
|
||||
show_max_index();
|
||||
|
||||
// Start Scanner Thread
|
||||
if (manual_search) {
|
||||
button_manual_search.set_text("SCAN"); // Update meaning of Manual Scan button
|
||||
text_current_desc.set("SEARCHING...");
|
||||
scan_thread = std::make_unique<ScannerThread>(frequency_range, field_step.selected_index_value());
|
||||
} else {
|
||||
button_manual_search.set_text("SRCH"); // Update meaning of Manual Scan button
|
||||
text_current_desc.set(loaded_filename());
|
||||
|
||||
// TODO: just pass ref to the thread?
|
||||
std::vector<rf::Frequency> frequency_list;
|
||||
frequency_list.reserve(entries.size());
|
||||
for (const auto& entry : entries)
|
||||
frequency_list.push_back(entry.freq);
|
||||
|
||||
scan_thread = std::make_unique<ScannerThread>(std::move(frequency_list));
|
||||
}
|
||||
|
||||
scan_thread->set_scanning_direction(fwd);
|
||||
}
|
||||
|
||||
void ScannerView::restart_scan() {
|
||||
audio::output::stop();
|
||||
if (scan_thread) // STOP SCANNER THREAD
|
||||
scan_thread->stop();
|
||||
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
start_scan_thread(); // RESTART SCANNER THREAD in selected mode
|
||||
}
|
||||
} // namespace ui::external_app::scanner
|
319
firmware/application/external/scanner/ui_scanner.hpp
vendored
Normal file
319
firmware/application/external/scanner/ui_scanner.hpp
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::scanner {
|
||||
|
||||
#define SCANNER_SLEEP_MS 50 // ms that Scanner Thread sleeps per loop
|
||||
#define STATISTICS_UPDATES_PER_SEC 10
|
||||
#define MAX_FREQ_LOCK 10 // # of 50ms cycles scanner locks into freq when signal detected, to verify signal is not spurious
|
||||
|
||||
// TODO: There is too much duplicated data in these classes.
|
||||
// ScannerThread should just use more from the View.
|
||||
// Or perhaps ScannerThread should just be in the View.
|
||||
|
||||
// TODO: Too many functions mix work and UI update.
|
||||
// Consolidate UI fixup to a single function.
|
||||
|
||||
// TODO: Just use freqman_entry.
|
||||
struct scanner_entry_t {
|
||||
rf::Frequency freq;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
struct scanner_range_t {
|
||||
int64_t min;
|
||||
int64_t max;
|
||||
};
|
||||
|
||||
class ScannerThread {
|
||||
public:
|
||||
ScannerThread(std::vector<rf::Frequency> frequency_list);
|
||||
ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz);
|
||||
~ScannerThread();
|
||||
|
||||
void set_scanning(const bool v);
|
||||
bool is_scanning();
|
||||
|
||||
void set_freq_lock(const uint32_t v);
|
||||
uint32_t is_freq_lock();
|
||||
|
||||
void set_freq_del(const rf::Frequency v);
|
||||
void set_index_stepper(const int32_t v);
|
||||
void set_scanning_direction(bool fwd);
|
||||
|
||||
void stop();
|
||||
|
||||
ScannerThread(const ScannerThread&) = delete;
|
||||
ScannerThread(ScannerThread&&) = delete;
|
||||
ScannerThread& operator=(const ScannerThread&) = delete;
|
||||
ScannerThread& operator=(ScannerThread&&) = delete;
|
||||
|
||||
private:
|
||||
std::vector<rf::Frequency> frequency_list_{};
|
||||
scanner_range_t frequency_range_{0, 0};
|
||||
size_t def_step_hz_{0};
|
||||
Thread* thread{nullptr};
|
||||
|
||||
bool _scanning{true};
|
||||
bool _manual_search{false};
|
||||
uint32_t _freq_lock{0};
|
||||
rf::Frequency _freq_del{0};
|
||||
uint32_t _freq_idx{0};
|
||||
int32_t _stepper{1};
|
||||
int32_t _index_stepper{0};
|
||||
static msg_t static_fn(void* arg);
|
||||
void run();
|
||||
void create_thread();
|
||||
};
|
||||
|
||||
class ScannerView : public View {
|
||||
public:
|
||||
ScannerView(NavigationView& nav);
|
||||
~ScannerView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Scanner"; };
|
||||
|
||||
private:
|
||||
static constexpr const char* default_freqman_file = "SCANNER";
|
||||
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
// Settings
|
||||
uint32_t browse_wait{5};
|
||||
uint32_t lock_wait{2};
|
||||
int32_t squelch{-30};
|
||||
scanner_range_t frequency_range{0, MAX_UFREQ};
|
||||
std::string freqman_file{default_freqman_file};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_scanner"sv,
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"browse_wait"sv, &browse_wait},
|
||||
{"lock_wait"sv, &lock_wait},
|
||||
{"scanner_squelch"sv, &squelch},
|
||||
{"range_min"sv, &frequency_range.min},
|
||||
{"range_max"sv, &frequency_range.max},
|
||||
{"file"sv, &freqman_file},
|
||||
}};
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
void start_scan_thread();
|
||||
void restart_scan();
|
||||
void change_mode(freqman_index_t mod_type);
|
||||
void show_max_index();
|
||||
void scan_pause();
|
||||
void scan_resume();
|
||||
void user_resume();
|
||||
void frequency_file_load(const std::filesystem::path& path);
|
||||
void bigdisplay_update(int32_t);
|
||||
void update_squelch_while_paused(int32_t max_db);
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void handle_retune(int64_t freq, uint32_t freq_idx);
|
||||
void handle_encoder(EncoderEvent delta);
|
||||
std::string loaded_filename() const;
|
||||
|
||||
uint32_t browse_timer{0};
|
||||
uint32_t lock_timer{0};
|
||||
uint32_t color_timer{0};
|
||||
int32_t bigdisplay_current_color{-2};
|
||||
rf::Frequency bigdisplay_current_frequency{0};
|
||||
|
||||
std::vector<scanner_entry_t> entries{};
|
||||
uint32_t current_index{0};
|
||||
rf::Frequency current_frequency{0};
|
||||
|
||||
bool userpause{false};
|
||||
bool manual_search{false};
|
||||
bool fwd{true}; // to preserve direction setting even if scan_thread restarted
|
||||
|
||||
enum bigdisplay_color_type {
|
||||
BDC_GREY,
|
||||
BDC_YELLOW,
|
||||
BDC_GREEN,
|
||||
BDC_RED
|
||||
};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "LNA: VGA: AMP: VOL:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 1 * 16}, "BW: SQ: Wsa: Wsl:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 10 * 16}, "SRCH START SEARCH END SWITCH", Theme::getInstance()->fg_light->foreground},
|
||||
|
||||
{{0 * 8, (26 * 8) + 4}, "MODE:", Theme::getInstance()->fg_light->foreground},
|
||||
{{11 * 8, (26 * 8) + 4}, "STEP:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{4 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{11 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{24 * 8, 0 * 16}};
|
||||
|
||||
OptionsField field_bw{
|
||||
{3 * 8, 1 * 16},
|
||||
6,
|
||||
{}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{13 * 8, 1 * 16},
|
||||
3,
|
||||
{-90, 20},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
NumberField field_browse_wait{
|
||||
// Signal-Active wait timer - time to wait before moving on even when signal locked
|
||||
{21 * 8, 1 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
NumberField field_lock_wait{
|
||||
// Signal-Lost wait timer - time to wait before moving on after losing signal lock
|
||||
{28 * 8, 1 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
RSSI rssi{
|
||||
{0 * 16, 2 * 16, 15 * 16, 8},
|
||||
};
|
||||
|
||||
TextField field_current_index{
|
||||
{0, 3 * 16, 3 * 8, 16},
|
||||
{},
|
||||
};
|
||||
|
||||
Text text_max_index{
|
||||
{4 * 8, 3 * 16, 18 * 8, 16},
|
||||
};
|
||||
|
||||
Text text_current_desc{
|
||||
{0, 4 * 16, 240 - 6 * 8, 16},
|
||||
};
|
||||
|
||||
BigFrequency big_display{
|
||||
{4, 6 * 16, 28 * 8, 52},
|
||||
0};
|
||||
|
||||
Button button_manual_start{
|
||||
{0 * 8, 11 * 16, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
Button button_manual_end{
|
||||
{12 * 8, 11 * 16, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
Button button_manual_search{
|
||||
{24 * 8, 11 * 16, 6 * 8, 28},
|
||||
""};
|
||||
|
||||
OptionsField field_mode{
|
||||
{5 * 8, (26 * 8) + 4},
|
||||
6,
|
||||
{} // Text strings get filled by freqman_set_modulation_option()
|
||||
};
|
||||
|
||||
OptionsField field_step{
|
||||
{17 * 8, (26 * 8) + 4},
|
||||
12,
|
||||
{} // Text strings get filled by freqman_set_step_option()
|
||||
};
|
||||
|
||||
ButtonWithEncoder button_pause{
|
||||
{0, (15 * 16) - 4, 72, 28},
|
||||
"<PAUSE>"};
|
||||
|
||||
Button button_dir{
|
||||
{0, (35 * 8) - 4, 72, 28},
|
||||
"REVERSE"};
|
||||
|
||||
Button button_audio_app{
|
||||
{84, (15 * 16) - 4, 72, 28},
|
||||
"AUDIO"};
|
||||
|
||||
Button button_mic_app{
|
||||
{84, (35 * 8) - 4, 72, 28},
|
||||
"MIC TX"};
|
||||
|
||||
Button button_add{
|
||||
{168, (15 * 16) - 4, 72, 28},
|
||||
"ADD FQ"};
|
||||
|
||||
Button button_load{
|
||||
{24 * 8, 3 * 16 - 10, 6 * 8, 22},
|
||||
"LOAD"};
|
||||
|
||||
Button button_clear{
|
||||
{24 * 8, 4 * 16, 6 * 8, 22},
|
||||
"MCLR"};
|
||||
|
||||
Button button_remove{
|
||||
{168, (35 * 8) - 4, 72, 28},
|
||||
"DEL FQ"};
|
||||
|
||||
std::unique_ptr<ScannerThread> scan_thread{};
|
||||
|
||||
MessageHandlerRegistration message_handler_retune{
|
||||
Message::ID::Retune,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const RetuneMessage*>(p);
|
||||
this->handle_retune(message.freq, message.range);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_stats{
|
||||
Message::ID::ChannelStatistics,
|
||||
[this](const Message* const p) {
|
||||
this->on_statistics_update(static_cast<const ChannelStatisticsMessage*>(p)->statistics);
|
||||
}};
|
||||
};
|
||||
} // namespace ui::external_app::scanner
|
Reference in New Issue
Block a user