mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 23:19:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2ad0a1b1a | |||
| 96cdb2e9e0 | |||
| 002ef720ee | |||
| 2214533894 | |||
| 06b7a0419e | |||
| d24ff7b3bc | |||
| a24b3ad3de | |||
| 91c6e3fc30 | |||
| 411f6c0a34 | |||
| 0a3aa706ef | |||
| 5ca74db2f9 | |||
| f24523c2f1 |
@@ -1 +1 @@
|
||||
v1.7.2
|
||||
v1.7.3
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.7.3
|
||||
v1.7.4
|
||||
|
||||
@@ -64,13 +64,19 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
|
||||
auto sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side.
|
||||
|
||||
/* Set up proper anti aliasing BPF bandwith in MAX2837 before ADC sampling according to the new added BW Options. */
|
||||
// For lower bandwidths, (12k5, 16k, 20k), increase the oversample rate to get a higher sample rate.
|
||||
OversampleRate oversample_rate = base_rate >= 25'000 ? OversampleRate::Rate8x : OversampleRate::Rate16x;
|
||||
|
||||
// HackRF suggests a minimum sample rate of 2M.
|
||||
// Oversampling helps get to higher sample rates when recording lower bandwidths.
|
||||
uint32_t sampling_rate = toUType(oversample_rate) * base_rate;
|
||||
|
||||
// Set up proper anti aliasing BPF bandwidth in MAX2837 before ADC sampling according to the new added BW Options.
|
||||
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
|
||||
|
||||
waterfall.stop();
|
||||
record_view.set_sampling_rate(sampling_rate);
|
||||
record_view.set_sampling_rate(sampling_rate, oversample_rate); // NB: Actually updates the baseband.
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
waterfall.start();
|
||||
|
||||
@@ -403,11 +403,17 @@ void FileManagerView::refresh_widgets(const bool v) {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void FileManagerView::on_rename() {
|
||||
void FileManagerView::on_rename(std::string_view hint) {
|
||||
auto& entry = get_selected_entry();
|
||||
name_buffer = entry.path.filename().string();
|
||||
uint32_t cursor_pos = (uint32_t)name_buffer.length();
|
||||
|
||||
// Append the hint to the filename stem as a rename suggestion.
|
||||
name_buffer = entry.path.stem().string();
|
||||
if (!hint.empty())
|
||||
name_buffer += "_" + std::string{hint};
|
||||
name_buffer += entry.path.extension().string();
|
||||
|
||||
// Set the rename cursor to before the extension to make renaming simpler.
|
||||
uint32_t cursor_pos = (uint32_t)name_buffer.length();
|
||||
if (auto pos = name_buffer.find_last_of(".");
|
||||
pos != name_buffer.npos && !entry.is_directory)
|
||||
cursor_pos = pos;
|
||||
@@ -434,6 +440,11 @@ void FileManagerView::on_rename() {
|
||||
}
|
||||
|
||||
void FileManagerView::on_delete() {
|
||||
if (is_directory(get_selected_full_path()) && !is_empty_directory(get_selected_full_path())) {
|
||||
nav_.display_modal("Delete", "Directory not empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = get_selected_entry().path.filename().string();
|
||||
nav_.push<ModalMessageView>(
|
||||
"Delete", "Delete " + name + "\nAre you sure?", YESNO,
|
||||
@@ -545,6 +556,7 @@ FileManagerView::FileManagerView(
|
||||
&button_new_dir,
|
||||
&button_new_file,
|
||||
&button_open_notepad,
|
||||
&button_rename_timestamp,
|
||||
});
|
||||
|
||||
menu_view.on_highlight = [this]() {
|
||||
@@ -568,7 +580,7 @@ FileManagerView::FileManagerView(
|
||||
|
||||
button_rename.on_select = [this]() {
|
||||
if (selected_is_valid())
|
||||
on_rename();
|
||||
on_rename("");
|
||||
};
|
||||
|
||||
button_delete.on_select = [this]() {
|
||||
@@ -614,6 +626,13 @@ FileManagerView::FileManagerView(
|
||||
} else
|
||||
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
||||
};
|
||||
|
||||
button_rename_timestamp.on_select = [this]() {
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||
on_rename(::truncate(to_string_timestamp(rtc_time::now()), 8));
|
||||
} else
|
||||
nav_.display_modal("Timestamp Rename", "Can't rename that.");
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -207,7 +207,7 @@ class FileManagerView : public FileManBaseView {
|
||||
ClipboardMode clipboard_mode{ClipboardMode::None};
|
||||
|
||||
void refresh_widgets(const bool v);
|
||||
void on_rename();
|
||||
void on_rename(std::string_view hint);
|
||||
void on_delete();
|
||||
void on_paste();
|
||||
void on_new_dir();
|
||||
@@ -272,6 +272,12 @@ class FileManagerView : public FileManBaseView {
|
||||
{},
|
||||
&bitmap_icon_notepad,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_rename_timestamp{
|
||||
{4 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_options_datetime,
|
||||
Color::orange()};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -487,7 +487,7 @@ void FrequencyEditView::populate_bandwidth_options() {
|
||||
|
||||
if (entry_.modulation < std::size(freqman_bandwidths)) {
|
||||
auto& bandwidths = freqman_bandwidths[entry_.modulation];
|
||||
for (auto i = 1u; i < bandwidths.size(); ++i) {
|
||||
for (auto i = 0u; i < bandwidths.size(); ++i) {
|
||||
auto& item = bandwidths[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
@@ -500,7 +500,7 @@ void FrequencyEditView::populate_step_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 1u; i < freqman_steps.size(); ++i) {
|
||||
for (auto i = 0u; i < freqman_steps.size(); ++i) {
|
||||
auto& item = freqman_steps[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
@@ -513,7 +513,7 @@ void FrequencyEditView::populate_tone_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 1u; i < tone_keys.size(); ++i) {
|
||||
for (auto i = 0u; i < tone_keys.size(); ++i) {
|
||||
auto& item = tone_keys[i];
|
||||
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
|
||||
}
|
||||
|
||||
@@ -219,12 +219,15 @@ class FrequencyEditView : public View {
|
||||
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_type{{13 * 8, 3 * 16}, 8, {
|
||||
{"Single", 0},
|
||||
{"Range", 1},
|
||||
{"HamRadio", 2},
|
||||
{"Raw", 3},
|
||||
}};
|
||||
OptionsField field_type{
|
||||
{13 * 8, 3 * 16},
|
||||
8,
|
||||
{
|
||||
{"Single", 0},
|
||||
{"Range", 1},
|
||||
{"HamRadio", 2},
|
||||
{"Raw", 3},
|
||||
}};
|
||||
|
||||
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
|
||||
|
||||
|
||||
@@ -1251,8 +1251,9 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
|
||||
}
|
||||
if (new_mod != SPEC_MODULATION) {
|
||||
button_audio_app.set_text("AUDIO");
|
||||
// TODO: Oversampling.
|
||||
record_view->set_sampling_rate(recording_sampling_rate);
|
||||
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorded
|
||||
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorted
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
} else {
|
||||
|
||||
@@ -69,10 +69,11 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
|
||||
button_save_freqs.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(freqman_dir.native()) == 0) {
|
||||
_output_file = new_file_path.stem().string();
|
||||
text_input_file.set(_output_file);
|
||||
button_output_file.set_text(_output_file);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
|
||||
@@ -21,20 +21,25 @@
|
||||
*/
|
||||
|
||||
#include "ui_scanner.hpp"
|
||||
|
||||
#include "optional.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
static const fs::path default_scan_file{u"FREQMAN/SCANNER.TXT"};
|
||||
|
||||
ScannerThread::ScannerThread(std::vector<rf::Frequency> frequency_list)
|
||||
: frequency_list_{std::move(frequency_list)} {
|
||||
_manual_search = false;
|
||||
create_thread();
|
||||
}
|
||||
|
||||
ScannerThread::ScannerThread(const jammer::jammer_range_t& frequency_range, size_t def_step_hz)
|
||||
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();
|
||||
@@ -80,7 +85,7 @@ void ScannerThread::set_freq_del(const rf::Frequency 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)
|
||||
// (used when rotary encoder is turned)
|
||||
void ScannerThread::set_index_stepper(const int32_t v) {
|
||||
_index_stepper = v;
|
||||
}
|
||||
@@ -228,17 +233,26 @@ void ScannerView::handle_retune(int64_t freq, uint32_t freq_idx) {
|
||||
}
|
||||
|
||||
if (!manual_search) {
|
||||
if (frequency_list.size() > 0) {
|
||||
if (entries.size() > 0)
|
||||
text_current_index.set(to_string_dec_uint(freq_idx + 1, 3));
|
||||
}
|
||||
|
||||
if (freq_idx < description_list.size() && description_list[freq_idx].size() > 1)
|
||||
desc_current_index.set(description_list[freq_idx]); // Show description from file
|
||||
if (freq_idx < entries.size() && entries[freq_idx].description.size() > 1)
|
||||
text_current_desc.set(entries[freq_idx].description); // Show description from file
|
||||
else
|
||||
desc_current_index.set(desc_freq_list_scan); // Show Scan file name (no description in file)
|
||||
text_current_desc.set(loaded_filename()); // Show Scan file name (no description in file)
|
||||
}
|
||||
}
|
||||
|
||||
std::string ScannerView::loaded_filename() const {
|
||||
auto filename = loaded_path.filename().string();
|
||||
if (filename.length() > 23) { // Truncate and add ellipses if long file name
|
||||
filename.resize(22);
|
||||
filename = filename + "+";
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
void ScannerView::focus() {
|
||||
button_load.focus();
|
||||
}
|
||||
@@ -254,46 +268,46 @@ ScannerView::~ScannerView() {
|
||||
void ScannerView::show_max_index() { // show total number of freqs to scan
|
||||
text_current_index.set("---");
|
||||
|
||||
if (frequency_list.size() == FREQMAN_MAX_PER_FILE) {
|
||||
if (entries.size() == FREQMAN_MAX_PER_FILE) {
|
||||
text_max_index.set_style(&Styles::red);
|
||||
text_max_index.set("/ " + to_string_dec_uint(FREQMAN_MAX_PER_FILE) + " (DB MAX!)");
|
||||
} else {
|
||||
text_max_index.set_style(&Styles::grey);
|
||||
text_max_index.set("/ " + to_string_dec_uint(frequency_list.size()));
|
||||
text_max_index.set("/ " + to_string_dec_uint(entries.size()));
|
||||
}
|
||||
}
|
||||
|
||||
ScannerView::ScannerView(
|
||||
NavigationView& nav)
|
||||
: nav_{nav}, loaded_file_name{"SCANNER"} {
|
||||
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,
|
||||
&text_current_index,
|
||||
&text_max_index,
|
||||
&desc_current_index,
|
||||
&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
|
||||
|
||||
: 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,
|
||||
&text_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
|
||||
@@ -312,14 +326,14 @@ ScannerView::ScannerView(
|
||||
frequency_range.max = stored_freq + 1000000;
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
|
||||
// Button to load txt files from the FREQMAN folder
|
||||
// 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(freqman_dir.native()) == 0) {
|
||||
scan_pause();
|
||||
frequency_file_load(new_file_path.stem().string(), true);
|
||||
frequency_file_load(new_file_path);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
@@ -328,14 +342,13 @@ ScannerView::ScannerView(
|
||||
|
||||
// Button to clear in-memory frequency list
|
||||
button_clear.on_select = [this, &nav](Button&) {
|
||||
if (scan_thread && frequency_list.size()) {
|
||||
if (scan_thread && entries.size()) {
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
frequency_list.clear();
|
||||
description_list.clear();
|
||||
entries.clear();
|
||||
|
||||
show_max_index(); // UPDATE new list size on screen
|
||||
text_current_index.set("");
|
||||
desc_current_index.set(desc_freq_list_scan);
|
||||
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)
|
||||
@@ -404,16 +417,15 @@ ScannerView::ScannerView(
|
||||
|
||||
// Button to delete current frequency from scan Freq List
|
||||
button_remove.on_select = [this](Button&) {
|
||||
if (scan_thread && (frequency_list.size() > current_index)) {
|
||||
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(frequency_list[current_index]);
|
||||
description_list.erase(description_list.begin() + current_index);
|
||||
frequency_list.erase(frequency_list.begin() + current_index);
|
||||
// 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
|
||||
desc_current_index.set(""); // Clean up description (cosmetic detail)
|
||||
text_current_desc.set(""); // Clean up description (cosmetic detail)
|
||||
scan_thread->set_freq_lock(0); // Reset the scanner lock
|
||||
}
|
||||
};
|
||||
@@ -432,15 +444,7 @@ ScannerView::ScannerView(
|
||||
manual_search = false; // Switch to List Scan mode
|
||||
}
|
||||
|
||||
audio::output::stop();
|
||||
|
||||
if (scan_thread)
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
start_scan_thread(); // RESTART SCANNER THREAD in selected mode
|
||||
restart_scan();
|
||||
};
|
||||
|
||||
// Mode field was changed (AM/NFM/WFM)
|
||||
@@ -467,17 +471,8 @@ ScannerView::ScannerView(
|
||||
field_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
|
||||
if (manual_search && scan_thread) {
|
||||
// Restart scan thread with new step value
|
||||
scan_thread->stop(); // STOP SCANNER THREAD
|
||||
|
||||
// Resuming pause automatically
|
||||
// FUTURE: Consider whether we should stay in Pause mode...
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
start_scan_thread(); // RESTART SCANNER THREAD in Manual Search mode
|
||||
}
|
||||
if (manual_search && scan_thread)
|
||||
restart_scan();
|
||||
};
|
||||
|
||||
// Button to toggle Forward/Reverse
|
||||
@@ -491,68 +486,37 @@ ScannerView::ScannerView(
|
||||
bigdisplay_update(BDC_GREY); // Back to grey color
|
||||
};
|
||||
|
||||
// TODO: remove this parsing?
|
||||
// Button to add current frequency (found during Search) to the Scan Frequency List
|
||||
button_add.on_select = [this](Button&) {
|
||||
File scanner_file;
|
||||
const std::string freq_file_path = "FREQMAN/" + loaded_file_name + ".TXT";
|
||||
auto result = scanner_file.open(freq_file_path); // First search if freq is already in txt
|
||||
FreqmanDB db;
|
||||
if (db.open(loaded_path, /*create*/ true)) {
|
||||
freqman_entry entry{
|
||||
.frequency_a = current_frequency,
|
||||
.type = freqman_type::Single,
|
||||
};
|
||||
|
||||
if (!result.is_valid()) {
|
||||
const std::string frequency_to_add = "f=" + to_string_dec_uint(current_frequency / 1000) + to_string_dec_uint(current_frequency % 1000UL, 3, '0');
|
||||
|
||||
bool found = false;
|
||||
constexpr size_t buffer_size = 1024;
|
||||
char buffer[buffer_size];
|
||||
|
||||
for (size_t pointer = 0, freq_str_idx = 0; pointer < scanner_file.size(); pointer += buffer_size) {
|
||||
size_t adjusted_buffer_size;
|
||||
if (pointer + buffer_size >= scanner_file.size()) {
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
adjusted_buffer_size = scanner_file.size() - pointer;
|
||||
} else {
|
||||
adjusted_buffer_size = buffer_size;
|
||||
}
|
||||
|
||||
scanner_file.seek(pointer);
|
||||
scanner_file.read(buffer, adjusted_buffer_size);
|
||||
|
||||
for (size_t i = 0; i < adjusted_buffer_size; ++i) {
|
||||
if (buffer[i] == frequency_to_add.data()[freq_str_idx]) {
|
||||
++freq_str_idx;
|
||||
if (freq_str_idx >= frequency_to_add.size()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
freq_str_idx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 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); // After showing an error
|
||||
bigdisplay_update(-1); // Need to poke this control after displaying modal?
|
||||
} else {
|
||||
scanner_file.append(freq_file_path); // Second: append if it is not there
|
||||
scanner_file.write_line(frequency_to_add);
|
||||
|
||||
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 (frequency_list.size() < FREQMAN_MAX_PER_FILE) {
|
||||
frequency_list.push_back(current_frequency);
|
||||
description_list.push_back("");
|
||||
|
||||
// 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 " + loaded_file_name + ".TXT\nfor appending freq.");
|
||||
bigdisplay_update(-1); // After showing an error
|
||||
nav_.display_modal("Error", "Cannot open " + loaded_path.filename().string() + "\nfor appending freq.");
|
||||
bigdisplay_update(-1); // Need to poke this control after displaying modal?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -566,106 +530,76 @@ ScannerView::ScannerView(
|
||||
field_squelch.on_change = [this](int32_t v) { squelch = v; };
|
||||
field_squelch.set_value(-30);
|
||||
|
||||
// LEARN FREQUENCIES
|
||||
std::string scanner_txt = "SCANNER";
|
||||
frequency_file_load(scanner_txt);
|
||||
// LOAD FREQUENCIES
|
||||
frequency_file_load(default_scan_file);
|
||||
}
|
||||
|
||||
void ScannerView::frequency_file_load(std::string file_name, bool stop_all_before) {
|
||||
bool found_range{false};
|
||||
bool found_single{false};
|
||||
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};
|
||||
|
||||
// stop everything running now if required
|
||||
if (stop_all_before) {
|
||||
scan_thread->stop();
|
||||
frequency_list.clear(); // clear the existing frequency list (expected behavior)
|
||||
description_list.clear();
|
||||
FreqmanDB db;
|
||||
if (!db.open(path)) {
|
||||
text_current_desc.set("NO " + path.filename().string());
|
||||
loaded_path = default_scan_file;
|
||||
return;
|
||||
}
|
||||
|
||||
if (load_freqman_file(file_name, database, {})) {
|
||||
loaded_file_name = file_name;
|
||||
for (auto& entry_ptr : database) {
|
||||
if (frequency_list.size() >= FREQMAN_MAX_PER_FILE)
|
||||
entries.clear();
|
||||
loaded_path = path;
|
||||
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::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;
|
||||
|
||||
auto& entry = *entry_ptr;
|
||||
|
||||
// Get modulation & bw & step from file if specified
|
||||
// Note these values could be different for each line in the file, but we only look at the first one
|
||||
//
|
||||
// Note that freqman requires a very specific string for these parameters,
|
||||
// so check syntax in frequency file if specified value isn't being loaded
|
||||
//
|
||||
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;
|
||||
|
||||
// Get frequency
|
||||
if (entry.type == freqman_type::Range) {
|
||||
if (!found_range) {
|
||||
// Set Start & End Search Range instead of populating the small in-memory frequency table
|
||||
// NOTE: There may be multiple single frequencies in file, but only one search range is supported.
|
||||
found_range = true;
|
||||
frequency_range.min = entry.frequency_a;
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
frequency_range.max = entry.frequency_b;
|
||||
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
|
||||
}
|
||||
} else if (entry.type == freqman_type::Single) {
|
||||
found_single = true;
|
||||
frequency_list.push_back(entry.frequency_a);
|
||||
description_list.push_back(entry.description);
|
||||
} else if (entry.type == freqman_type::HamRadio) {
|
||||
// For HAM repeaters, add both receive & transmit frequencies to scan list and modify description
|
||||
// (FUTURE fw versions might handle these differently)
|
||||
found_single = true;
|
||||
frequency_list.push_back(entry.frequency_a);
|
||||
description_list.push_back("R:" + entry.description);
|
||||
|
||||
if ((entry.frequency_a != entry.frequency_b) &&
|
||||
(frequency_list.size() < FREQMAN_MAX_PER_FILE)) {
|
||||
frequency_list.push_back(entry.frequency_b);
|
||||
description_list.push_back("T:" + entry.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loaded_file_name = "SCANNER"; // back to the default frequency file
|
||||
desc_current_index.set(" NO " + file_name + ".TXT FILE ...");
|
||||
}
|
||||
|
||||
desc_freq_list_scan = loaded_file_name + ".TXT";
|
||||
if (desc_freq_list_scan.length() > 23) { // Truncate description and add ellipses if long file name
|
||||
desc_freq_list_scan.resize(20);
|
||||
desc_freq_list_scan = desc_freq_list_scan + "...";
|
||||
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); // Update mode (also triggers a change callback that disables & reenables RF background)
|
||||
field_mode.set_by_value(def_mod_index);
|
||||
|
||||
if (is_valid(def_bw_index)) // Update BW if specified in file
|
||||
if (is_valid(def_bw_index))
|
||||
field_bw.set_selected_index(def_bw_index);
|
||||
|
||||
if (is_valid(def_step_index)) // Update step if specified in file
|
||||
if (is_valid(def_step_index))
|
||||
field_step.set_selected_index(def_step_index);
|
||||
|
||||
audio::output::stop();
|
||||
// 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));
|
||||
}
|
||||
|
||||
if (userpause) // If user-paused, resume
|
||||
user_resume();
|
||||
|
||||
// Scan list if we found one, otherwise do manual range search
|
||||
manual_search = !found_single;
|
||||
|
||||
start_scan_thread();
|
||||
// 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) {
|
||||
@@ -754,7 +688,8 @@ void ScannerView::user_resume() {
|
||||
userpause = false; // Resume scanning
|
||||
}
|
||||
|
||||
void ScannerView::change_mode(freqman_index_t new_mod) { // Before this, do a scan_thread->stop(); After this do a start_scan_thread()
|
||||
// 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;
|
||||
@@ -791,22 +726,41 @@ void ScannerView::change_mode(freqman_index_t new_mod) { // Before this, do a s
|
||||
|
||||
void ScannerView::start_scan_thread() {
|
||||
receiver_model.enable();
|
||||
// Disable squelch on the model because RSSI handler is where the
|
||||
// actual squelching is applied for this app.
|
||||
receiver_model.set_squelch_level(0);
|
||||
show_max_index();
|
||||
|
||||
// Start Scanner Thread
|
||||
// FUTURE: Consider passing additional control flags (fwd,userpause,etc) to scanner thread at start (perhaps in a data structure)
|
||||
if (manual_search) {
|
||||
button_manual_search.set_text("SCAN"); // Update meaning of Manual Scan button
|
||||
desc_current_index.set(desc_freq_range_search);
|
||||
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
|
||||
desc_current_index.set(desc_freq_list_scan);
|
||||
scan_thread = std::make_unique<ScannerThread>(frequency_list);
|
||||
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 */
|
||||
|
||||
@@ -20,19 +20,20 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#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"
|
||||
#include "ui_styles.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
#define SCANNER_SLEEP_MS 50 // ms that Scanner Thread sleeps per loop
|
||||
#define STATISTICS_UPDATES_PER_SEC 10
|
||||
@@ -40,10 +41,28 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
// 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 jammer::jammer_range_t& frequency_range, size_t def_step_hz);
|
||||
ScannerThread(const scanner_range_t& frequency_range, size_t def_step_hz);
|
||||
~ScannerThread();
|
||||
|
||||
void set_scanning(const bool v);
|
||||
@@ -65,7 +84,7 @@ class ScannerThread {
|
||||
|
||||
private:
|
||||
std::vector<rf::Frequency> frequency_list_{};
|
||||
jammer::jammer_range_t frequency_range_{false, 0, 0};
|
||||
scanner_range_t frequency_range_{0, 0};
|
||||
size_t def_step_hz_{0};
|
||||
Thread* thread{nullptr};
|
||||
|
||||
@@ -89,10 +108,6 @@ class ScannerView : public View {
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Scanner"; };
|
||||
std::vector<rf::Frequency> frequency_list{};
|
||||
std::vector<std::string> description_list{};
|
||||
|
||||
// void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
private:
|
||||
app_settings::SettingsManager settings_{
|
||||
@@ -102,18 +117,21 @@ class ScannerView : public View {
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
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(std::string file_name, bool stop_all_before = false);
|
||||
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);
|
||||
|
||||
jammer::jammer_range_t frequency_range{false, 0, 0}; // perfect for manual scan task too...
|
||||
std::string loaded_filename() const;
|
||||
|
||||
scanner_range_t frequency_range{0, 0};
|
||||
int32_t squelch{0};
|
||||
uint32_t browse_timer{0};
|
||||
uint32_t lock_timer{0};
|
||||
@@ -122,10 +140,12 @@ class ScannerView : public View {
|
||||
rf::Frequency bigdisplay_current_frequency{0};
|
||||
uint32_t browse_wait{0};
|
||||
uint32_t lock_wait{0};
|
||||
freqman_db database{};
|
||||
std::string loaded_file_name;
|
||||
|
||||
std::filesystem::path loaded_path{};
|
||||
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
|
||||
@@ -137,9 +157,6 @@ class ScannerView : public View {
|
||||
BDC_RED
|
||||
};
|
||||
|
||||
std::string desc_freq_range_search = "SEARCHING...";
|
||||
std::string desc_freq_list_scan = "";
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "LNA: VGA: AMP: VOL:", Color::light_grey()},
|
||||
{{0 * 8, 1 * 16}, "BW: SQ: Wsa: Wsl:", Color::light_grey()},
|
||||
@@ -204,7 +221,7 @@ class ScannerView : public View {
|
||||
{4 * 8, 3 * 16, 18 * 8, 16},
|
||||
};
|
||||
|
||||
Text desc_current_index{
|
||||
Text text_current_desc{
|
||||
{0, 4 * 16, 240 - 6 * 8, 16},
|
||||
};
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ static void show_save_prompt(
|
||||
std::function<void()> on_save,
|
||||
std::function<void()> continuation) {
|
||||
nav.display_modal(
|
||||
"Save?", "Save changes?", YESNO,
|
||||
"Save?", " Save changes?", YESNO,
|
||||
[on_save](bool choice) {
|
||||
if (choice && on_save)
|
||||
on_save();
|
||||
@@ -474,7 +474,13 @@ void TextEditorView::open_file(const fs::path& path) {
|
||||
path_ = {};
|
||||
file_dirty_ = false;
|
||||
has_temp_file_ = false;
|
||||
auto result = FileWrapper::open(path);
|
||||
auto result = FileWrapper::open(
|
||||
path, false, [](uint32_t value, uint32_t total) {
|
||||
Painter p;
|
||||
auto percent = (value * 100) / total;
|
||||
auto width = (percent * screen_width) / 100;
|
||||
p.draw_hline({0, 16}, width, Color::yellow());
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||
@@ -582,6 +588,10 @@ void TextEditorView::prepare_for_write() {
|
||||
if (has_temp_file_)
|
||||
return;
|
||||
|
||||
// TODO: This would be nice to have but it causes a stack overflow in an ISR?
|
||||
// Painter p;
|
||||
// p.draw_string({2, 48}, Styles::yellow, "Creating temporary file...");
|
||||
|
||||
// Copy to temp file on write.
|
||||
has_temp_file_ = true;
|
||||
delete_temp_file(path_);
|
||||
|
||||
@@ -352,6 +352,11 @@ void set_sample_rate(const uint32_t sample_rate) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_oversample_rate(OversampleRate oversample_rate) {
|
||||
OversampleRateConfigMessage message{oversample_rate};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void capture_start(CaptureConfig* const config) {
|
||||
CaptureConfigMessage message{config};
|
||||
send_message(&message);
|
||||
|
||||
@@ -95,6 +95,7 @@ void spectrum_streaming_start();
|
||||
void spectrum_streaming_stop();
|
||||
|
||||
void set_sample_rate(const uint32_t sample_rate);
|
||||
void set_oversample_rate(OversampleRate oversample_rate);
|
||||
void capture_start(CaptureConfig* const config);
|
||||
void capture_stop();
|
||||
void replay_start(ReplayConfig* const config);
|
||||
|
||||
@@ -5607,6 +5607,44 @@ static constexpr Bitmap bitmap_icon_speaker_and_headphones_mute{
|
||||
{16, 16},
|
||||
bitmap_icon_speaker_and_headphones_mute_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_shift_data[] = {
|
||||
0x00,
|
||||
0x00,
|
||||
0x80,
|
||||
0x00,
|
||||
0xC0,
|
||||
0x01,
|
||||
0xE0,
|
||||
0x03,
|
||||
0xF0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x0F,
|
||||
0xFC,
|
||||
0x1F,
|
||||
0xE0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x03,
|
||||
0x20,
|
||||
0x02,
|
||||
0xE0,
|
||||
0x03,
|
||||
0x20,
|
||||
0x02,
|
||||
0xE0,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
};
|
||||
static constexpr Bitmap bitmap_icon_shift{
|
||||
{16, 16},
|
||||
bitmap_icon_shift_data};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__BITMAP_HPP__*/
|
||||
|
||||
@@ -568,6 +568,17 @@ bool is_directory(const path& file_path) {
|
||||
return fr == FR_OK && is_directory(static_cast<file_status>(filinfo.fattrib));
|
||||
}
|
||||
|
||||
bool is_empty_directory(const path& file_path) {
|
||||
DIR dir;
|
||||
FILINFO filinfo;
|
||||
|
||||
if (!is_directory(file_path))
|
||||
return false;
|
||||
|
||||
auto result = f_findfirst(&dir, &filinfo, reinterpret_cast<const TCHAR*>(file_path.c_str()), (const TCHAR*)u"*");
|
||||
return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0'));
|
||||
}
|
||||
|
||||
space_info space(const path& p) {
|
||||
DWORD free_clusters{0};
|
||||
FATFS* fs;
|
||||
|
||||
@@ -248,6 +248,7 @@ bool is_directory(const file_status s);
|
||||
bool is_regular_file(const file_status s);
|
||||
bool file_exists(const path& file_path);
|
||||
bool is_directory(const path& file_path);
|
||||
bool is_empty_directory(const path& file_path);
|
||||
|
||||
space_info space(const path& p);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
@@ -75,6 +76,8 @@ class BufferWrapper {
|
||||
}
|
||||
virtual ~BufferWrapper() {}
|
||||
|
||||
std::function<void(Size, Size)> on_read_progress{};
|
||||
|
||||
/* Prevent copies */
|
||||
BufferWrapper(const BufferWrapper&) = delete;
|
||||
BufferWrapper& operator=(const BufferWrapper&) = delete;
|
||||
@@ -234,13 +237,23 @@ class BufferWrapper {
|
||||
|
||||
line_count_ = start_line_;
|
||||
Offset offset = start_offset_;
|
||||
|
||||
// Report progress every N lines.
|
||||
constexpr auto report_interval = 100u;
|
||||
auto result = next_newline(offset);
|
||||
auto next_report = report_interval;
|
||||
|
||||
while (result) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
|
||||
if (on_read_progress && line_count_ > next_report) {
|
||||
on_read_progress(offset, size());
|
||||
next_report = line_count_ + report_interval;
|
||||
}
|
||||
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
@@ -397,6 +410,9 @@ class BufferWrapper {
|
||||
// Number of bytes left to shift.
|
||||
Offset remaining = size() - src;
|
||||
Offset offset = size();
|
||||
Size report_total = remaining;
|
||||
Size report_interval = report_total / 8;
|
||||
Size next_report = remaining - report_interval;
|
||||
|
||||
while (remaining > 0) {
|
||||
offset -= std::min(remaining, buffer_size);
|
||||
@@ -413,6 +429,11 @@ class BufferWrapper {
|
||||
break;
|
||||
|
||||
remaining -= *result;
|
||||
|
||||
if (on_read_progress && remaining <= next_report) {
|
||||
on_read_progress(report_total - remaining, report_total);
|
||||
next_report = remaining > report_interval ? remaining - report_interval : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +445,9 @@ class BufferWrapper {
|
||||
|
||||
char buffer[buffer_size];
|
||||
auto offset = src;
|
||||
Size report_total = size();
|
||||
Size report_interval = report_total / 8;
|
||||
Size next_report = offset + report_interval;
|
||||
|
||||
while (true) {
|
||||
wrapped_->seek(offset);
|
||||
@@ -438,6 +462,11 @@ class BufferWrapper {
|
||||
break;
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (on_read_progress && offset >= next_report) {
|
||||
on_read_progress(offset, report_total);
|
||||
next_report = offset + report_interval;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the extra bytes at the end of the file.
|
||||
@@ -463,13 +492,19 @@ class FileWrapper : public BufferWrapper<File, 64> {
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
using Error = File::Error;
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path, bool create = false) {
|
||||
static Result<std::unique_ptr<FileWrapper>> open(
|
||||
const std::filesystem::path& path,
|
||||
bool create = false,
|
||||
std::function<void(Size, Size)> on_read_progress = nullptr) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path, /*read_only*/ false, create);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
|
||||
if (on_read_progress)
|
||||
fw->on_read_progress = on_read_progress;
|
||||
|
||||
fw->initialize();
|
||||
return fw;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ options_t freqman_bandwidths[4] = {
|
||||
},
|
||||
{
|
||||
// SPEC -- TODO: these should be indexes.
|
||||
{"8k5", 8500},
|
||||
{"11k", 11000},
|
||||
{"12k5", 12500},
|
||||
{"16k", 16000},
|
||||
{"20k", 20000},
|
||||
{"25k", 25000},
|
||||
{"50k", 50000},
|
||||
{"100k", 100000},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -24,9 +25,8 @@
|
||||
#include "utility.hpp"
|
||||
|
||||
uint8_t Debounce::state() {
|
||||
bool v = !pulse_upon_release_ && (state_ || simulated_pulse_);
|
||||
if (simulated_pulse_)
|
||||
simulated_pulse_ = false;
|
||||
bool v = state_to_report_;
|
||||
simulated_pulse_ = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -52,84 +52,100 @@ bool Debounce::long_press_occurred() {
|
||||
bool Debounce::feed(const uint8_t bit) {
|
||||
history_ = (history_ << 1) | (bit & 1);
|
||||
|
||||
// "Repeat" handling - simulated button release
|
||||
if (repeat_ctr_) {
|
||||
// Make sure the button is still being held continuously
|
||||
if ((history_ == 0xFF) && !long_press_enabled_) {
|
||||
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
|
||||
if (--repeat_ctr_ == 0) {
|
||||
state_ = !state_;
|
||||
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// It's a real button release; stop simulating
|
||||
repeat_ctr_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (state_ == 0) {
|
||||
// Previous button state was 0 (released);
|
||||
// Has button been held for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
|
||||
// Has button been held for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == DEBOUNCE_MASK) {
|
||||
//
|
||||
// Button is currently pressed;
|
||||
// Was previous button state 0 (released)?
|
||||
//
|
||||
if (state_ == 0) {
|
||||
//
|
||||
// Button has been pressed (after filtering glitches), transition 0->1
|
||||
//
|
||||
state_ = 1;
|
||||
held_time_ = 0;
|
||||
|
||||
// If long_press_enabled_, state() function masks the button press until it's released
|
||||
// or until LONG_PRESS_DELAY is reached
|
||||
if (long_press_enabled_) {
|
||||
pulse_upon_release_ = true;
|
||||
state_to_report_ = 0;
|
||||
return false;
|
||||
}
|
||||
state_to_report_ = 1;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Previous button state was 1 (pressed);
|
||||
// Has button been released for DEBOUNCE_COUNT ticks?
|
||||
if ((history_ & DEBOUNCE_MASK) == 0) {
|
||||
// Button has been released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
|
||||
|
||||
// "Repeat" handling - simulated button release
|
||||
if (repeat_ctr_ && !long_press_enabled_) {
|
||||
// Simulate button press every REPEAT_SUBSEQUENT_DELAY ticks
|
||||
// (by toggling reported state every 1/2 of the delay time)
|
||||
if (--repeat_ctr_ == 0) {
|
||||
state_to_report_ = !state_to_report_;
|
||||
repeat_ctr_ = REPEAT_SUBSEQUENT_DELAY / 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of how long button has been held
|
||||
held_time_++;
|
||||
if (pulse_upon_release_) {
|
||||
// Button is being held down and long_press support is enabled for this key:
|
||||
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
|
||||
// indicating it was a LONG press
|
||||
// (note that repeat_support and long_press support are mutually exclusive)
|
||||
if (held_time_ >= LONG_PRESS_DELAY) {
|
||||
pulse_upon_release_ = false;
|
||||
long_press_occurred_ = true;
|
||||
state_to_report_ = 1;
|
||||
return true;
|
||||
}
|
||||
} else if (repeat_enabled_ && !long_press_enabled_) {
|
||||
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
|
||||
if (held_time_ >= REPEAT_INITIAL_DELAY) {
|
||||
// Delay reached; trigger repeat code on NEXT tick
|
||||
repeat_ctr_ = 1;
|
||||
held_time_ = 0;
|
||||
}
|
||||
}
|
||||
} else if ((history_ & DEBOUNCE_MASK) == 0) { // Has button been released for at least DEBOUNCE_COUNT ticks?
|
||||
//
|
||||
// Button is released;
|
||||
// Was previous button state 1 (pressed)?
|
||||
//
|
||||
if (state_ == 1) {
|
||||
//
|
||||
// Button has been released (after filtering glitches), transition 1->0
|
||||
//
|
||||
state_ = 0;
|
||||
long_press_occurred_ = false;
|
||||
|
||||
// If button released when long_press_enabled_ and before LONG_PRESS_DELAY was reached;
|
||||
// allow state() function to finally return a single press indication (simulated pulse).
|
||||
// Note: In long press mode, apps won't see button press until the button is released.
|
||||
if (pulse_upon_release_) {
|
||||
// force state() function (called by EventDispatcher) to return simulated press for one cycle
|
||||
simulated_pulse_ = true;
|
||||
pulse_upon_release_ = false;
|
||||
} else {
|
||||
state_ = 0;
|
||||
simulated_pulse_ = true;
|
||||
state_to_report_ = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset long_press_occurred_ flag after button is released
|
||||
long_press_occurred_ = false;
|
||||
state_to_report_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Has button been held continuously?
|
||||
if (history_ == 0xFF) {
|
||||
held_time_++;
|
||||
if (pulse_upon_release_) {
|
||||
// Button is being held down and long_press support is enabled for this key:
|
||||
// if LONG_PRESS_DELAY is reached then finally report that switch is pressed and set flag
|
||||
// indicating it was a LONG press
|
||||
// (note that repeat_support and long_press support are mutually exclusive)
|
||||
if (held_time_ >= LONG_PRESS_DELAY) {
|
||||
long_press_occurred_ = true;
|
||||
simulated_pulse_ = true;
|
||||
pulse_upon_release_ = false;
|
||||
held_time_ = 0;
|
||||
return true;
|
||||
}
|
||||
} else if (repeat_enabled_ && !long_press_enabled_) {
|
||||
// Repeat support -- 4 directional buttons only (unless long_press is enabled)
|
||||
if (held_time_ == REPEAT_INITIAL_DELAY) {
|
||||
// Delay reached; trigger repeat code on NEXT tick
|
||||
repeat_ctr_ = 1;
|
||||
held_time_ = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Button not continuously pressed; reset counter
|
||||
held_time_ = 0;
|
||||
// Reset reported state after application/event has cleared simulated_pulse_
|
||||
if (state_to_report_ == 1 && !simulated_pulse_) {
|
||||
state_to_report_ = 0;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// Button is in transition between states;
|
||||
// Reset counters until button inputs are stable.
|
||||
//
|
||||
held_time_ = 0;
|
||||
repeat_ctr_ = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,15 +43,25 @@ class Debounce {
|
||||
bool long_press_occurred();
|
||||
|
||||
private:
|
||||
uint8_t history_{0};
|
||||
uint8_t state_{0};
|
||||
bool repeat_enabled_{false};
|
||||
uint16_t repeat_ctr_{0};
|
||||
uint16_t held_time_{0};
|
||||
bool pulse_upon_release_{false};
|
||||
bool simulated_pulse_{false};
|
||||
bool long_press_enabled_{false};
|
||||
bool long_press_occurred_{false};
|
||||
uint8_t history_{0}; // shift register of last 8 reads from button hardware state bit
|
||||
|
||||
uint8_t state_{0}; // actual button hardware state (after debounce logic), 1=pressed
|
||||
|
||||
uint8_t state_to_report_{0}; // pseudo button state reported by state() function (may be masked off or simulated presses)
|
||||
|
||||
bool repeat_enabled_{false}; // TRUE if this button is enabled to auto-repeat when held down (ignored if long_press_enabled)
|
||||
|
||||
uint16_t repeat_ctr_{0}; // used for timing auto-repeat simulated button presses when button is held down and repeat_enabled
|
||||
|
||||
uint16_t held_time_{0}; // number of ticks that the button has been held down (compared against REPEAT and LONG_PRESS delays)
|
||||
|
||||
bool pulse_upon_release_{false}; // TRUE when button is being held down when long_press_enabled and LONG_PRESS_DELAY hasn't been reached yet
|
||||
|
||||
bool simulated_pulse_{false}; // TRUE if a simulated button press is active following a short button press (only when long_press_enabled)
|
||||
|
||||
bool long_press_enabled_{false}; // TRUE when button is in long-press mode (takes precedence over the repeat_enabled flag)
|
||||
|
||||
bool long_press_occurred_{false}; // TRUE when button is being held down and LONG_PRESS_DELAY has been reached (only when long_press_enabled)
|
||||
};
|
||||
|
||||
#endif /*__DEBOUNCE_H__*/
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
|
||||
#include "ui_alphanum.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include "utility.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -38,6 +37,7 @@ AlphanumView::AlphanumView(
|
||||
size_t n;
|
||||
|
||||
add_children({
|
||||
&button_shift,
|
||||
&labels,
|
||||
&field_raw,
|
||||
&text_raw_to_char,
|
||||
@@ -49,6 +49,19 @@ AlphanumView::AlphanumView(
|
||||
this->on_button(button);
|
||||
};
|
||||
|
||||
button_shift.set_vertical_center(true);
|
||||
button_shift.on_select = [this]() {
|
||||
incr(shift_mode);
|
||||
|
||||
// Disallow one-time shift in digits/symbols mode
|
||||
if ((mode == 1) && (shift_mode == ShiftMode::Shift))
|
||||
shift_mode = ShiftMode::ShiftLock;
|
||||
else if (shift_mode > ShiftMode::ShiftLock)
|
||||
shift_mode = ShiftMode::None;
|
||||
|
||||
refresh_keys();
|
||||
};
|
||||
|
||||
n = 0;
|
||||
for (auto& button : buttons) {
|
||||
button.id = n;
|
||||
@@ -56,9 +69,9 @@ AlphanumView::AlphanumView(
|
||||
focused_button = button.id;
|
||||
};
|
||||
button.on_select = button_fn;
|
||||
button.set_parent_rect({static_cast<Coord>((n % 5) * (240 / 5)),
|
||||
button.set_parent_rect({static_cast<Coord>((n % 5) * (screen_width / 5)),
|
||||
static_cast<Coord>((n / 5) * 38 + 24),
|
||||
240 / 5, 38});
|
||||
screen_width / 5, 38});
|
||||
add_child(&button);
|
||||
n++;
|
||||
}
|
||||
@@ -78,38 +91,59 @@ AlphanumView::AlphanumView(
|
||||
char_add(field_raw.value());
|
||||
};
|
||||
|
||||
// make text_raw_to_char widget display the char value from field_raw
|
||||
field_raw.on_change = [this](auto) {
|
||||
text_raw_to_char.set(std::string{static_cast<char>(field_raw.value())});
|
||||
};
|
||||
}
|
||||
|
||||
void AlphanumView::set_mode(const uint32_t new_mode) {
|
||||
size_t n = 0;
|
||||
|
||||
if (new_mode < 3)
|
||||
void AlphanumView::set_mode(uint32_t new_mode, ShiftMode new_shift_mode) {
|
||||
if (new_mode < std::size(key_sets))
|
||||
mode = new_mode;
|
||||
else
|
||||
mode = 0;
|
||||
|
||||
const char* key_list = key_sets[mode].second;
|
||||
shift_mode = new_shift_mode;
|
||||
refresh_keys();
|
||||
|
||||
if (mode + 1 < std::size(key_sets))
|
||||
button_mode.set_text(key_sets[mode + 1].name);
|
||||
else
|
||||
button_mode.set_text(key_sets[0].name);
|
||||
}
|
||||
|
||||
void AlphanumView::refresh_keys() {
|
||||
auto key_list = shift_mode == ShiftMode::None
|
||||
? key_sets[mode].normal
|
||||
: key_sets[mode].shifted;
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& button : buttons) {
|
||||
const std::string label{
|
||||
key_list[n]};
|
||||
button.set_text(label);
|
||||
button.set_text(std::string{key_list[n]});
|
||||
n++;
|
||||
}
|
||||
|
||||
if (mode < 2)
|
||||
button_mode.set_text(key_sets[mode + 1].first);
|
||||
else
|
||||
button_mode.set_text(key_sets[0].first);
|
||||
switch (shift_mode) {
|
||||
case ShiftMode::None:
|
||||
button_shift.set_color(Color::dark_grey());
|
||||
break;
|
||||
case ShiftMode::Shift:
|
||||
button_shift.set_color(Color::black());
|
||||
break;
|
||||
case ShiftMode::ShiftLock:
|
||||
button_shift.set_color(Color::dark_blue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AlphanumView::on_button(Button& button) {
|
||||
const auto c = button.text()[0];
|
||||
char_add(c);
|
||||
|
||||
// TODO: Consolidate shift handling.
|
||||
if (shift_mode == ShiftMode::Shift) {
|
||||
shift_mode = ShiftMode::None;
|
||||
refresh_keys();
|
||||
}
|
||||
}
|
||||
|
||||
bool AlphanumView::on_encoder(const EncoderEvent delta) {
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
#include "ui_textentry.hpp"
|
||||
#include "ui_menu.hpp"
|
||||
|
||||
// TODO: Building this as a custom widget instead of using
|
||||
// all the Button controls would save a considerable amount of RAM.
|
||||
// The Buttons each have a backing string but these only need one char.
|
||||
|
||||
namespace ui {
|
||||
|
||||
class AlphanumView : public TextEntryView {
|
||||
@@ -43,22 +47,42 @@ class AlphanumView : public TextEntryView {
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
|
||||
private:
|
||||
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, ._";
|
||||
const char* const keys_lower = "abcdefghijklmnopqrstuvwxyz, ._";
|
||||
const char* const keys_digit = "0123456789!\"#'()*+-/:;=<>@[\\]?";
|
||||
enum class ShiftMode : uint8_t {
|
||||
None,
|
||||
Shift,
|
||||
ShiftLock,
|
||||
};
|
||||
|
||||
const std::pair<std::string, const char*> key_sets[3] = {
|
||||
{"ABC", keys_upper},
|
||||
{"abc", keys_lower},
|
||||
{"123", keys_digit}};
|
||||
const char* const keys_lower = "abcdefghijklmnopqrstuvwxyz, .";
|
||||
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, .";
|
||||
const char* const keys_digit = "1234567890()'`\"+-*/=<>_\\!?, .";
|
||||
const char* const keys_symbl = "!@#$%^&*()[]'`\"{}|:;<>-_~?, .";
|
||||
|
||||
struct key_set_t {
|
||||
const char* name;
|
||||
const char* normal;
|
||||
const char* shifted;
|
||||
};
|
||||
|
||||
const key_set_t key_sets[2] = {
|
||||
{"abc", keys_lower, keys_upper},
|
||||
{"123", keys_digit, keys_symbl}};
|
||||
|
||||
int16_t focused_button = 0;
|
||||
uint32_t mode = 0; // Uppercase
|
||||
uint32_t mode = 0; // Letters.
|
||||
ShiftMode shift_mode = ShiftMode::None;
|
||||
|
||||
void set_mode(const uint32_t new_mode);
|
||||
void set_mode(uint32_t new_mode, ShiftMode new_shift_mode = ShiftMode::None);
|
||||
void refresh_keys();
|
||||
void on_button(Button& button);
|
||||
|
||||
std::array<Button, 30> buttons{};
|
||||
std::array<Button, 29> buttons{};
|
||||
|
||||
NewButton button_shift{
|
||||
{192, 214, screen_width / 5, 38},
|
||||
{},
|
||||
&bitmap_icon_shift,
|
||||
Color::dark_grey()};
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 33 * 8}, "Raw:", Color::light_grey()},
|
||||
@@ -76,11 +100,11 @@ class AlphanumView : public TextEntryView {
|
||||
"0"};
|
||||
|
||||
Button button_delete{
|
||||
{9 * 8, 33 * 8, 6 * 8, 32},
|
||||
{9 * 8, 32 * 8 - 3, 7 * 8, 3 * 16 + 3},
|
||||
"<DEL"};
|
||||
|
||||
Button button_mode{
|
||||
{16 * 8, 33 * 8, 5 * 8, 32},
|
||||
{16 * 8, 32 * 8 - 3, 6 * 8, 3 * 16 + 3},
|
||||
""};
|
||||
};
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
*/
|
||||
|
||||
#include "ui_textentry.hpp"
|
||||
//#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_alphanum.hpp"
|
||||
//#include "ui_handwrite.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class TextEntryView : public View {
|
||||
|
||||
TextEdit text_input;
|
||||
Button button_ok{
|
||||
{22 * 8, 33 * 8, 7 * 8, 32},
|
||||
{22 * 8, 32 * 8 - 3, 8 * 8, 3 * 16 + 3},
|
||||
"OK"};
|
||||
};
|
||||
|
||||
|
||||
@@ -101,24 +101,27 @@ void RecordView::focus() {
|
||||
button_record.focus();
|
||||
}
|
||||
|
||||
void RecordView::set_sampling_rate(const size_t new_sampling_rate) {
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
|
||||
void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_oversample_rate) {
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
|
||||
where we are NOT recording full IQ .C16 files (recorded files are decimated ones).
|
||||
Those decimated recorded files,has not the full IQ samples .
|
||||
are ok as recorded spectrum indication, but they should not be used by Replay app.
|
||||
Those decimated recorded files, has not the full IQ samples.
|
||||
are ok as recorded spectrum indication, but they should not be used by Replay app.
|
||||
|
||||
We keep original black background in all the correct IQ .C16 files BW's Options */
|
||||
if (new_sampling_rate > 4800000) { // > BW >600kHz (fs=8*BW), (750kHz ...2750kHz)
|
||||
We keep original black background in all the correct IQ .C16 files BW's Options */
|
||||
if (new_sampling_rate > 4'800'000) { // > BW >600kHz (fs=8*BW), (750kHz...2750kHz)
|
||||
button_record.set_background(ui::Color::yellow());
|
||||
} else {
|
||||
button_record.set_background(ui::Color::black());
|
||||
}
|
||||
|
||||
if (new_sampling_rate != sampling_rate) {
|
||||
if (new_sampling_rate != sampling_rate ||
|
||||
new_oversample_rate != oversample_rate) {
|
||||
stop();
|
||||
|
||||
sampling_rate = new_sampling_rate;
|
||||
oversample_rate = new_oversample_rate;
|
||||
baseband::set_sample_rate(sampling_rate);
|
||||
baseband::set_oversample_rate(oversample_rate);
|
||||
|
||||
button_record.hidden(sampling_rate == 0);
|
||||
text_record_filename.hidden(sampling_rate == 0);
|
||||
@@ -162,12 +165,13 @@ void RecordView::start() {
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
|
||||
// ISO 8601
|
||||
std::string date_time = to_string_dec_uint(datetime.year(), 4, '0') +
|
||||
to_string_dec_uint(datetime.month(), 2, '0') +
|
||||
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
|
||||
to_string_dec_uint(datetime.hour()) +
|
||||
to_string_dec_uint(datetime.minute()) +
|
||||
to_string_dec_uint(datetime.second());
|
||||
std::string date_time =
|
||||
to_string_dec_uint(datetime.year(), 4, '0') +
|
||||
to_string_dec_uint(datetime.month(), 2, '0') +
|
||||
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
|
||||
to_string_dec_uint(datetime.hour()) +
|
||||
to_string_dec_uint(datetime.minute()) +
|
||||
to_string_dec_uint(datetime.second());
|
||||
|
||||
base_path = filename_stem_pattern.string() + "_" + date_time + "_" +
|
||||
trim(to_string_freq(receiver_model.target_frequency())) + "Hz";
|
||||
@@ -197,10 +201,9 @@ void RecordView::start() {
|
||||
|
||||
case FileType::RawS8:
|
||||
case FileType::RawS16: {
|
||||
const auto metadata_file_error =
|
||||
write_metadata_file(get_metadata_path(base_path),
|
||||
{receiver_model.target_frequency(), sampling_rate / 8});
|
||||
// Not sure why sample_rate is div. 8, but stored value matches rate settings.
|
||||
const auto metadata_file_error = write_metadata_file(
|
||||
get_metadata_path(base_path),
|
||||
{receiver_model.target_frequency(), sampling_rate / toUType(oversample_rate)});
|
||||
if (metadata_file_error.is_valid()) {
|
||||
handle_error(metadata_file_error.value());
|
||||
return;
|
||||
@@ -263,13 +266,24 @@ void RecordView::update_status_display() {
|
||||
text_record_dropped.set(s);
|
||||
}
|
||||
|
||||
/*if (pitch_rssi_enabled) {
|
||||
button_pitch_rssi.invert_colors();
|
||||
}*/
|
||||
/*
|
||||
if (pitch_rssi_enabled) {
|
||||
button_pitch_rssi.invert_colors();
|
||||
}
|
||||
*/
|
||||
|
||||
if (sampling_rate) {
|
||||
if (sampling_rate > 0) {
|
||||
const auto space_info = std::filesystem::space(u"");
|
||||
const uint32_t bytes_per_second = file_type == FileType::WAV ? (sampling_rate * 2) : (sampling_rate / 8 * 4); // TODO: Why 8/4??
|
||||
// - Audio is 1 int16_t per sample or '2' bytes per sample.
|
||||
// - C8 captures 2 (I,Q) int8_t per sample or '2' bytes per sample.
|
||||
// - C16 captures 2 (I,Q) int16_t per sample or '4' bytes per sample.
|
||||
const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2;
|
||||
// WAV files are not oversampled, but C8 and C16 are. Divide by the
|
||||
// oversample rate to get the effective sample rate.
|
||||
const auto effective_sampling_rate = file_type == FileType::WAV
|
||||
? sampling_rate
|
||||
: sampling_rate / toUType(oversample_rate);
|
||||
const uint32_t bytes_per_second = effective_sampling_rate * bytes_per_sample;
|
||||
const uint32_t available_seconds = space_info.free / bytes_per_second;
|
||||
const uint32_t seconds = available_seconds % 60;
|
||||
const uint32_t available_minutes = available_seconds / 60;
|
||||
|
||||
@@ -42,8 +42,7 @@ class RecordView : public View {
|
||||
enum FileType {
|
||||
RawS8 = 1,
|
||||
RawS16 = 2,
|
||||
RawS32 = 3,
|
||||
WAV = 4,
|
||||
WAV = 3,
|
||||
};
|
||||
|
||||
RecordView(
|
||||
@@ -57,7 +56,16 @@ class RecordView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
void set_sampling_rate(const size_t new_sampling_rate);
|
||||
/* Sets the sampling rate and the oversampling "decimation" rate.
|
||||
* These values are passed down to the baseband proc_capture. For
|
||||
* Audio (WAV) recording, the OversampleRate should not be
|
||||
* specified and the default will be used. */
|
||||
/* TODO: Currently callers are expected to have already multiplied the
|
||||
* sample_rate with the oversample rate. It would be better move that
|
||||
* logic to a single place. */
|
||||
void set_sampling_rate(
|
||||
size_t new_sampling_rate,
|
||||
OversampleRate new_oversample_rate = OversampleRate::Rate8x);
|
||||
|
||||
void set_file_type(const FileType v) { file_type = v; }
|
||||
|
||||
@@ -91,6 +99,7 @@ class RecordView : public View {
|
||||
const size_t write_size;
|
||||
const size_t buffer_count;
|
||||
size_t sampling_rate{0};
|
||||
OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Rectangle rect_background{
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
#include "utility.hpp"
|
||||
|
||||
CaptureProcessor::CaptureProcessor() {
|
||||
decim_0.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_0_4.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_0_8.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_200k_decim_1.taps, 131072);
|
||||
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
@@ -36,7 +37,7 @@ CaptureProcessor::CaptureProcessor() {
|
||||
|
||||
void CaptureProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
const auto decim_0_out = decim_0_execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto& decimator_out = decim_1_out;
|
||||
const auto& channel = decimator_out;
|
||||
@@ -65,9 +66,19 @@ void CaptureProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig:
|
||||
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
|
||||
case Message::ID::SamplerateConfig: {
|
||||
auto config = reinterpret_cast<const SamplerateConfigMessage*>(message);
|
||||
baseband_fs = config->sample_rate;
|
||||
update_for_rate_change();
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::OversampleRateConfig: {
|
||||
auto config = reinterpret_cast<const OversampleRateConfigMessage*>(message);
|
||||
oversample_rate = config->oversample_rate;
|
||||
update_for_rate_change();
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::CaptureConfig:
|
||||
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
|
||||
@@ -78,11 +89,14 @@ void CaptureProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
}
|
||||
|
||||
void CaptureProcessor::samplerate_config(const SamplerateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate;
|
||||
void CaptureProcessor::update_for_rate_change() {
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
|
||||
size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
|
||||
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x
|
||||
? decim_0_4.decimation_factor
|
||||
: decim_0_8.decimation_factor;
|
||||
|
||||
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
|
||||
|
||||
size_t decim_1_input_fs = decim_0_output_fs;
|
||||
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
||||
@@ -103,6 +117,20 @@ void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
|
||||
}
|
||||
}
|
||||
|
||||
buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) {
|
||||
switch (oversample_rate) {
|
||||
case OversampleRate::Rate8x:
|
||||
return decim_0_4.execute(src, dst);
|
||||
|
||||
case OversampleRate::Rate16x:
|
||||
return decim_0_8.execute(src, dst);
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled OversampleRate");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<CaptureProcessor>()};
|
||||
event_dispatcher.run();
|
||||
|
||||
@@ -50,7 +50,13 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||
/* NB: There are two decimation passes: 0 and 1. In pass 0, one of
|
||||
* the following will be selected based on the oversample rate.
|
||||
* use decim_0_4 for an overall decimation factor of 8.
|
||||
* use decim_0_8 for an overall decimation factor of 16. */
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0_4{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8{};
|
||||
|
||||
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
@@ -61,14 +67,19 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
SpectrumCollector channel_spectrum{};
|
||||
size_t spectrum_interval_samples = 0;
|
||||
size_t spectrum_samples = 0;
|
||||
OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
void samplerate_config(const SamplerateConfigMessage& message);
|
||||
/* Called to update members when the sample rate or oversample rate is changed. */
|
||||
void update_for_rate_change();
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
|
||||
/* Dispatch to the correct decim_0 based on oversample rate. */
|
||||
buffer_c16_t decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst);
|
||||
};
|
||||
|
||||
#endif /*__PROC_CAPTURE_HPP__*/
|
||||
|
||||
@@ -46,6 +46,8 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||
|
||||
if (!configured || !stream) return;
|
||||
|
||||
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / 8};
|
||||
|
||||
// File data is in C16 format, we need C8
|
||||
// File samplerate is 500kHz, we're at 4MHz
|
||||
// iq_buffer can only be 512 C16 samples (RAM limitation)
|
||||
|
||||
@@ -45,10 +45,6 @@ class ReplayProcessor : public BasebandProcessor {
|
||||
static constexpr auto spectrum_rate_hz = 50.0f;
|
||||
|
||||
std::array<complex16_t, 256> iq{};
|
||||
const buffer_c16_t iq_buffer{
|
||||
iq.data(),
|
||||
iq.size(),
|
||||
baseband_fs / 8};
|
||||
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
|
||||
@@ -111,6 +111,7 @@ class Message {
|
||||
APRSRxConfigure = 54,
|
||||
SpectrumPainterBufferRequestConfigure = 55,
|
||||
SpectrumPainterBufferResponseConfigure = 56,
|
||||
OversampleRateConfig = 57,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -809,6 +810,23 @@ class SamplerateConfigMessage : public Message {
|
||||
const uint32_t sample_rate = 0;
|
||||
};
|
||||
|
||||
/* Controls decimation handling in proc_capture. */
|
||||
enum class OversampleRate : uint8_t {
|
||||
Rate8x = 8,
|
||||
Rate16x = 16,
|
||||
};
|
||||
|
||||
class OversampleRateConfigMessage : public Message {
|
||||
public:
|
||||
constexpr OversampleRateConfigMessage(
|
||||
OversampleRate oversample_rate)
|
||||
: Message{ID::OversampleRateConfig},
|
||||
oversample_rate(oversample_rate) {
|
||||
}
|
||||
|
||||
const OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
};
|
||||
|
||||
class AudioLevelReportMessage : public Message {
|
||||
public:
|
||||
constexpr AudioLevelReportMessage()
|
||||
|
||||
@@ -1188,7 +1188,7 @@ void NewButton::paint(Painter& painter) {
|
||||
painter.draw_bitmap(
|
||||
bmp_pos,
|
||||
*bitmap_,
|
||||
color_, // Color::green(), //fg,
|
||||
color_,
|
||||
bg);
|
||||
}
|
||||
|
||||
@@ -1667,8 +1667,6 @@ void TextEdit::char_delete() {
|
||||
}
|
||||
|
||||
void TextEdit::paint(Painter& painter) {
|
||||
constexpr int char_width = 8;
|
||||
|
||||
auto rect = screen_rect();
|
||||
auto text_style = has_focus() ? style().invert() : style();
|
||||
auto offset = 0;
|
||||
@@ -1677,15 +1675,14 @@ void TextEdit::paint(Painter& painter) {
|
||||
if (cursor_pos_ >= char_count_)
|
||||
offset = cursor_pos_ - char_count_ + 1;
|
||||
|
||||
// Clear the control.
|
||||
painter.fill_rectangle(rect, text_style.background);
|
||||
|
||||
// Draw the text starting at the offset.
|
||||
for (uint32_t i = 0; i < char_count_ && i + offset < text_.length(); i++) {
|
||||
for (uint32_t i = 0; i < char_count_; i++) {
|
||||
// Using draw_char to blank the rest of the line with spaces produces less flicker.
|
||||
auto c = (i + offset < text_.length()) ? text_[i + offset] : ' ';
|
||||
|
||||
painter.draw_char(
|
||||
{rect.location().x() + (static_cast<int>(i) * char_width), rect.location().y()},
|
||||
text_style,
|
||||
text_[i + offset]);
|
||||
text_style, c);
|
||||
}
|
||||
|
||||
// Determine cursor position on screen (either the cursor position or the last char).
|
||||
@@ -1698,7 +1695,7 @@ void TextEdit::paint(Painter& painter) {
|
||||
painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]);
|
||||
|
||||
// Draw the cursor.
|
||||
Rect cursor_box{cursor_point, {char_width, 16}};
|
||||
Rect cursor_box{cursor_point, {char_width, char_height}};
|
||||
painter.draw_rectangle(cursor_box, cursor_style.background);
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 267 B |
@@ -435,4 +435,29 @@ SCENARIO("Delete line.") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("It calls on_read_progress while reading.") {
|
||||
GIVEN("A file larger than internal buffer_size (512)") {
|
||||
std::string content = std::string(599, 'a');
|
||||
content.push_back('x');
|
||||
MockFile f{content};
|
||||
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
auto called = false;
|
||||
|
||||
w.on_read_progress = [&called](auto, auto) {
|
||||
called = true;
|
||||
};
|
||||
|
||||
WHEN("Replacing range with larger size") {
|
||||
w.replace_range({0, 2}, "bbb");
|
||||
|
||||
THEN("callback should be called.") {
|
||||
CHECK(called);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
Reference in New Issue
Block a user