Compare commits

...

14 Commits

Author SHA1 Message Date
Mark Thompson 91c6e3fc30 Display error message when trying to delete non-empty directory (#1321)
* Non-empty directory check

* Non-empty directory check

* Display error when attempting to delete non-empty directory

* Clang

Delete white-space that the friggin editor added

* is_empty_directory

* is_empty_directory

* is_empty_directory

* Now need to check if it's a directory first
2023-07-30 15:46:59 -05:00
Kyle Reed 411f6c0a34 Progress bar for Notepad IO (#1322) 2023-07-30 09:36:57 +02:00
Kyle Reed 0a3aa706ef Scanner cleanup (#1320)
* FreqmanDB in Scanner

* Format

* Add comment on squelch(0)
2023-07-29 15:36:00 -07:00
Kyle Reed 5ca74db2f9 Fix output file config in recon (#1319) 2023-07-29 07:16:49 +02:00
Kyle Reed f24523c2f1 Add button to prompt rename with a timestamp. (#1315) 2023-07-28 18:53:16 -07:00
Mark Thompson e7c5a862da Revert encoder sensitivity change (#1314) 2023-07-27 16:45:39 +02:00
Mark Thompson b27c738b69 XOR cursor support in Notepad (#1311)
* XOR cursor support in Notepad

* XOR cursor support

* XOR cursor support

* Revert change

* Use static buffer

* Use static buffer

* Clang
2023-07-27 09:14:02 -05:00
jLynx 8c565bbf15 Allow changelog to run without a token (#1312)
* Allow code to run without a token

* Reverted submodule change by mistake
2023-07-27 14:53:56 +12:00
Mark Thompson 37aa9c046f Gps default frequency (#1309)
* Default frequency

* Default frequency

* Use sample rate from .ini file if present

* Eliminate unneeded sampling_rate variable

* Eliminate unneeded sampling_rate variable
2023-07-26 13:52:27 -05:00
Kyle Reed 195a6224a0 Freq field tweaks (#1306)
* UX improvements

* Prevent wrapping
2023-07-25 08:06:13 -05:00
Mark Thompson ea238f4988 Improve Select button response when Long Press is enabled during CPU-intensive apps (WFM) (#1304)
* Long press

* Long-press improvement during CPU-intensive apps

* Long-press improvement during CPU-intensive apps
2023-07-24 20:45:24 -07:00
Kyle Reed 3514a9a608 Digit Mode for frequency field (#1298)
* Remove 'auto' step mode

* Support per-digit edits on the freq field.

* Swizzle instead of raw accessor

* Fix debug ui after swizzle
2023-07-24 18:09:22 +02:00
Mark Thompson e2bca9aebb Restore missing line (#1302) 2023-07-24 10:12:35 -05:00
Mark Thompson 6ae164e59b Show scale on map (#1296)
* Add scale to geomap

* Add scale to geomap
2023-07-24 14:52:30 +02:00
36 changed files with 793 additions and 434 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ def print_nightly_changelog():
def handle_get_request(path, offset=None):
headers = {"Authorization": f"Bearer {token}"}
headers = {} if token == None else {"Authorization": f"Bearer {token}"}
params = {"since": offset}
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
response = requests.get(url_base + path, headers=headers, params=params)
@@ -48,9 +48,9 @@ class AMOptionsView : public View {
OptionsField options_config{
{3 * 8, 0 * 16},
6, // number of blanking characters
6, // Max option length
{
// using common messages from freqman.cpp
// Using common messages from freqman_ui.cpp
}};
};
@@ -65,9 +65,9 @@ class NBFMOptionsView : public View {
};
OptionsField options_config{
{3 * 8, 0 * 16},
4,
3, // Max option length
{
// using common messages from freqman.cpp
// Using common messages from freqman_ui.cpp
}};
Text text_squelch{
@@ -93,9 +93,9 @@ class WFMOptionsView : public View {
};
OptionsField options_config{
{3 * 8, 0 * 16},
4,
4, // Max option length
{
// using common messages from freqman.cpp
// Using common messages from freqman_ui.cpp
}};
};
@@ -51,6 +51,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
field_frequency_step.set_by_value(receiver_model.frequency_step());
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
receiver_model.set_frequency_step(v);
this->field_frequency.set_step(v);
};
option_format.set_selected_index(0); // Default to C16
+8 -6
View File
@@ -63,17 +63,15 @@ void GpsSimAppView::on_file_changed(const fs::path& new_file_path) {
if (metadata) {
field_frequency.set_value(metadata->center_frequency);
sample_rate = metadata->sample_rate;
} else {
sample_rate = 2600000;
transmitter_model.set_sampling_rate(metadata->sample_rate);
}
// UI Fixup.
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 1) + "Hz");
text_sample_rate.set(unit_auto_scale(transmitter_model.sampling_rate(), 3, 1) + "Hz");
progressbar.set_max(file_size);
text_filename.set(truncate(file_path.filename().string(), 12));
auto duration = ms_duration(file_size, sample_rate, 2);
auto duration = ms_duration(file_size, transmitter_model.sampling_rate(), 2);
text_duration.set(to_string_time_ms(duration));
button_play.focus();
@@ -129,7 +127,6 @@ void GpsSimAppView::start() {
});
}
transmitter_model.set_sampling_rate(sample_rate);
transmitter_model.enable();
}
@@ -176,6 +173,11 @@ GpsSimAppView::GpsSimAppView(
&waterfall,
});
if (!settings_.loaded()) {
field_frequency.set_value(initial_target_frequency);
transmitter_model.set_sampling_rate(2600000);
}
field_frequency.set_step(5000);
button_play.on_select = [this](ImageButton&) {
+2 -1
View File
@@ -54,6 +54,8 @@ class GpsSimAppView : public View {
std::string title() const override { return "GPS Sim TX"; };
private:
static constexpr uint32_t initial_target_frequency = 1575420000;
NavigationView& nav_;
RxRadioState radio_state_{
3000000 /* bandwidth */,
@@ -64,7 +66,6 @@ class GpsSimAppView : public View {
static constexpr ui::Dim header_height = 3 * 16;
uint32_t sample_rate = 0;
int32_t tx_gain{47};
bool rf_amp{true}; // aux private var to store temporal, same as Replay App rf_amp user selection.
static constexpr uint32_t baseband_bandwidth = 3000000; // filter bandwidth
+5 -5
View File
@@ -251,7 +251,7 @@ void ControlsSwitchesWidget::on_show() {
bool ControlsSwitchesWidget::on_key(const KeyEvent key) {
key_event_mask = 1 << toUType(key);
long_press_key_event_mask = switch_long_press_occurred((size_t)key) ? key_event_mask : 0;
long_press_key_event_mask = key_is_long_pressed(key) ? key_event_mask : 0;
return true;
}
@@ -264,9 +264,9 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
{32, 64, 16, 16}, // Down
{32, 0, 16, 16}, // Up
{32, 32, 16, 16}, // Select
{96, 0, 16, 16}, // Dfu
{16, 96, 16, 16}, // Encoder phase 0
{48, 96, 16, 16}, // Encoder phase 1
{96, 0, 16, 16}, // Dfu
{96, 64, 16, 16}, // Touch
}};
@@ -283,9 +283,9 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
{32 + 1, 64 + 1, 16 - 2, 16 - 2}, // Down
{32 + 1, 0 + 1, 16 - 2, 16 - 2}, // Up
{32 + 1, 32 + 1, 16 - 2, 16 - 2}, // Select
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
{16 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 0
{48 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 1
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
}};
auto switches_raw = control::debug::switches();
@@ -354,13 +354,13 @@ DebugControlsView::DebugControlsView(NavigationView& nav) {
});
button_done.on_select = [&nav](Button&) {
switches_long_press_enable(0);
set_switches_long_press_config(0);
nav.pop();
};
options_switches_mode.on_change = [this](size_t, OptionsField::value_t v) {
(void)v;
switches_long_press_enable(options_switches_mode.selected_index_value());
set_switches_long_press_config(options_switches_mode.selected_index_value());
};
}
+23 -4
View File
@@ -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
+7 -1
View File
@@ -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 */
+3 -3
View File
@@ -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});
}
+9 -6
View File
@@ -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}};
@@ -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.");
}
+159 -205
View File
@@ -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 */
+41 -24
View File
@@ -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},
};
+3 -5
View File
@@ -480,12 +480,10 @@ class SetEncoderDialView : public View {
OptionsField field_encoder_dial_sensitivity{
{20 * 8, 3 * 16},
7,
{{"LOWEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOWEST},
{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
6,
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH},
{"HIGHEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGHEST}}};
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
Button button_save{
{2 * 8, 16 * 16, 12 * 8, 32},
+40 -14
View File
@@ -72,6 +72,7 @@ void TextViewer::paint(Painter& painter) {
paint_state_.first_line = first_line;
paint_state_.first_col = first_col;
paint_state_.redraw_text = true;
paint_state_.line = UINT32_MAX; // forget old cursor position when overwritten
}
if (paint_state_.redraw_text) {
@@ -221,6 +222,10 @@ void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
r.top() + (int)i * char_height,
clear_width * char_width, char_height},
style().background);
// if cursor line got overwritten, disable XOR of old cursor when displaying new cursor
if (i == paint_state_.line)
paint_state_.line = UINT32_MAX;
}
}
@@ -228,21 +233,32 @@ void TextViewer::paint_cursor(Painter& painter) {
if (!has_focus())
return;
auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) {
auto r = screen_rect();
line = line - paint_state_.first_line;
col = col - paint_state_.first_col;
auto xor_cursor = [this, &painter](int32_t line, uint16_t col) {
int cursor_width = char_width + 1;
int x = (col - paint_state_.first_col) * char_width - 1;
if (x < 0) { // cursor is one pixel narrower when in left column
cursor_width--;
x = 0;
}
int y = screen_rect().top() + (line - paint_state_.first_line) * char_height;
painter.draw_rectangle(
{(int)col * char_width - 1,
r.top() + (int)line * char_height,
char_width + 1, char_height},
c);
// Converting one row at a time to reduce buffer size
auto pbuf8 = cursor_.pixel_buffer8;
auto pbuf = cursor_.pixel_buffer;
for (auto col = 0; col < char_height; col++) {
// Annoyingly, read_pixels uses a 24-bit pixel format vs draw_pixels which uses 16-bit
portapack::display.read_pixels({x, y + col, cursor_width, 1}, pbuf8, cursor_width);
for (auto i = 0; i < cursor_width; i++)
pbuf[i] = Color(pbuf8[i].r, pbuf8[i].g, pbuf8[i].b).v ^ 0xFFFF;
portapack::display.draw_pixels({x, y + col, cursor_width, 1}, pbuf, cursor_width);
}
};
// Clear old cursor. CONSIDER: XOR cursor?
draw_cursor(paint_state_.line, paint_state_.col, style().background);
draw_cursor(cursor_.line, cursor_.col, style().foreground);
if (paint_state_.line != UINT32_MAX) // only XOR old cursor if it still appears on the screen
xor_cursor(paint_state_.line, paint_state_.col);
xor_cursor(cursor_.line, cursor_.col);
paint_state_.line = cursor_.line;
paint_state_.col = cursor_.col;
}
@@ -325,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();
@@ -458,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());
@@ -566,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_);
@@ -117,6 +117,10 @@ class TextViewer : public Widget {
uint32_t line{};
uint16_t col{};
ScrollDirection dir{ScrollDirection::Vertical};
// Pixel buffer used for cursor XOR'ing - Max cursor width = Max char width + 1
ColorRGB888 pixel_buffer8[ui::char_width + 1]{};
Color pixel_buffer[ui::char_width + 1]{};
} cursor_{};
};
+11
View File
@@ -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;
+1
View File
@@ -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);
+36 -1
View File
@@ -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;
}
+34 -7
View File
@@ -23,6 +23,31 @@
#include "utility.hpp"
uint8_t Debounce::state() {
bool v = !pulse_upon_release_ && (state_ || simulated_pulse_);
if (simulated_pulse_)
simulated_pulse_ = false;
return v;
}
void Debounce::enable_repeat() {
repeat_enabled_ = true;
}
bool Debounce::get_long_press_enabled() const {
return long_press_enabled_;
}
void Debounce::set_long_press_enabled(bool v) {
long_press_enabled_ = v;
}
bool Debounce::long_press_occurred() {
bool v = long_press_occurred_;
long_press_occurred_ = false;
return v;
}
// Returns TRUE if button state changed (after debouncing)
bool Debounce::feed(const uint8_t bit) {
history_ = (history_ << 1) | (bit & 1);
@@ -63,11 +88,12 @@ bool Debounce::feed(const uint8_t bit) {
// 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;
// allow state() function to finally return a single press indication
// (in long press mode, apps won't see button press until the button is released)
// 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_) {
// leaving state_==1 for one cycle
pulse_upon_release_ = 0;
// force state() function (called by EventDispatcher) to return simulated press for one cycle
simulated_pulse_ = true;
pulse_upon_release_ = false;
} else {
state_ = 0;
}
@@ -84,10 +110,11 @@ bool Debounce::feed(const uint8_t bit) {
// 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 repease_support and long_press support are mutually exclusive)
if (held_time_ == LONG_PRESS_DELAY) {
// (note that repeat_support and long_press support are mutually exclusive)
if (held_time_ >= LONG_PRESS_DELAY) {
long_press_occurred_ = true;
pulse_upon_release_ = 0;
simulated_pulse_ = true;
pulse_upon_release_ = false;
held_time_ = 0;
return true;
}
+11 -23
View File
@@ -31,39 +31,27 @@
// # of timer0 ticks before a held button starts being counted as repeated presses
#define REPEAT_INITIAL_DELAY 250
#define REPEAT_SUBSEQUENT_DELAY 92
#define LONG_PRESS_DELAY 1000
#define LONG_PRESS_DELAY 800
class Debounce {
public:
bool feed(const uint8_t bit);
uint8_t state() const {
return (pulse_upon_release_) ? 0 : state_;
}
void enable_repeat() {
repeat_enabled_ = true;
}
void set_long_press_support(bool v) {
long_press_enabled_ = v;
}
bool long_press_occurred() {
bool v = long_press_occurred_;
long_press_occurred_ = false;
return v;
}
uint8_t state();
void enable_repeat();
bool get_long_press_enabled() const;
void set_long_press_enabled(bool v);
bool long_press_occurred();
private:
uint8_t history_{0};
uint8_t state_{0};
bool repeat_enabled_{0};
bool repeat_enabled_{false};
uint16_t repeat_ctr_{0};
uint16_t held_time_{0};
bool pulse_upon_release_{0};
bool long_press_enabled_{0};
bool long_press_occurred_{0};
bool pulse_upon_release_{false};
bool simulated_pulse_{false};
bool long_press_enabled_{false};
bool long_press_occurred_{false};
};
#endif /*__DEBOUNCE_H__*/
+62 -27
View File
@@ -32,24 +32,66 @@
// 12 degrees of rotation.
//
// For each encoder "pulse" there are 4 state transitions, and we can choose
// how many transitions are needed before movement is registered
static const int8_t transition_map[16] = {
0, // 0000: noop
-1, // 0001: ccw start
1, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
-1, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
1, // 1011: cw end
0, // 1100: rate
1, // 1101: cw start
-1, // 1110: ccw start
0, // 1111: noop
// between looking at all of them (high sensitivity), half of them (medium/default),
// or one quarter of them (low sensitivity).
static const int8_t transition_map[][16] = {
// Normal (Medium) Sensitivity -- default
{
0, // 0000: noop
0, // 0001: ccw start
0, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
-1, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
1, // 1011: cw end
0, // 1100: rate
0, // 1101: cw start
0, // 1110: ccw start
0, // 1111: noop
},
// Low Sensitivity
{
0, // 0000: noop
0, // 0001: ccw start
0, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
0, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
0, // 1011: cw end
0, // 1100: rate
0, // 1101: cw start
0, // 1110: ccw start
0, // 1111: noop
},
// High Sensitivity
{
0, // 0000: noop
-1, // 0001: ccw start
1, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
-1, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
1, // 1011: cw end
0, // 1100: rate
1, // 1101: cw start
-1, // 1110: ccw start
0, // 1111: noop
},
};
int_fast8_t Encoder::update(
@@ -60,13 +102,6 @@ int_fast8_t Encoder::update(
state <<= 1;
state |= phase_1;
int_fast8_t retval = transition_map[state & 0xf];
transition_count += retval;
if (abs(transition_count) > portapack::persistent_memory::config_encoder_dial_sensitivity())
transition_count = 0;
else
retval = 0;
return retval;
// dial sensitivity setting is stored in pmem
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
}
-1
View File
@@ -32,7 +32,6 @@ class Encoder {
private:
uint_fast8_t state{0};
int_fast8_t transition_count{0};
};
#endif /*__ENCODER_H__*/
+81 -43
View File
@@ -22,32 +22,33 @@
#include "irq_controls.hpp"
#include "ch.h"
#include "hal.h"
#include "debounce.hpp"
#include "encoder.hpp"
#include "event_m0.hpp"
#include "hal.h"
#include "touch.hpp"
#include "touch_adc.hpp"
#include "encoder.hpp"
#include "debounce.hpp"
#include "utility.hpp"
#include <cstdint>
#include <array>
#include "portapack_io.hpp"
#include "hackrf_hal.hpp"
using namespace hackrf::one;
using namespace portapack;
static Thread* thread_controls_event = NULL;
static std::array<Debounce, 8> switch_debounce;
// Index with the Switch enum.
static std::array<Debounce, 6> switch_debounce;
static std::array<Debounce, 2> encoder_debounce;
static_assert(std::size(switch_debounce) == toUType(Switch::Dfu) + 1);
static Encoder encoder;
static volatile uint32_t encoder_position{0};
static volatile uint32_t touch_phase{0};
/* TODO: Change how touch scanning works. It produces a decent amount of noise
@@ -60,11 +61,11 @@ static volatile uint32_t touch_phase{0};
* Noise will only occur when the panel is being touched. Not ideal, but
* an acceptable improvement.
*/
static std::array<portapack::IO::TouchPinsConfig, 3> touch_pins_configs{
static std::array<IO::TouchPinsConfig, 3> touch_pins_configs{
/* State machine will pause here until touch is detected. */
portapack::IO::TouchPinsConfig::SensePressure,
portapack::IO::TouchPinsConfig::SenseX,
portapack::IO::TouchPinsConfig::SenseY,
IO::TouchPinsConfig::SensePressure,
IO::TouchPinsConfig::SenseX,
IO::TouchPinsConfig::SenseY,
};
static touch::Frame temp_frame;
@@ -80,7 +81,7 @@ static bool touch_update() {
const auto current_phase = touch_pins_configs[touch_phase];
switch (current_phase) {
case portapack::IO::TouchPinsConfig::SensePressure: {
case IO::TouchPinsConfig::SensePressure: {
const auto z1 = samples.xp - samples.xn;
const auto z2 = samples.yp - samples.yn;
const auto touch_raw = (z1 > touch::touch_threshold) || (z2 > touch::touch_threshold);
@@ -94,11 +95,11 @@ static bool touch_update() {
}
} break;
case portapack::IO::TouchPinsConfig::SenseX:
case IO::TouchPinsConfig::SenseX:
temp_frame.x += samples;
break;
case portapack::IO::TouchPinsConfig::SenseY:
case IO::TouchPinsConfig::SenseY:
temp_frame.y += samples;
break;
@@ -122,25 +123,57 @@ static bool touch_update() {
static uint8_t switches_raw = 0;
/* The raw data is not packed in a way that makes looping over it easy.
* One option would be an accessor helper (RawSwitch). Another option
* is to swizzle the bits into a friendlier order. */
// /* Type to access the bits in the raw switch data. */
// struct RawSwitch {
// const uint8_t raw_{0};
// uint8_t right() const { return (raw_ >> 0) & 1; }
// uint8_t left() const { return (raw_ >> 1) & 1; }
// uint8_t down() const { return (raw_ >> 2) & 1; }
// uint8_t up() const { return (raw_ >> 3) & 1; }
// uint8_t select() const { return (raw_ >> 4) & 1; }
// uint8_t rot_a() const { return (raw_ >> 5) & 1; }
// uint8_t rot_b() const { return (raw_ >> 6) & 1; }
// uint8_t dfu() const { return (raw_ >> 7) & 1; }};
static uint8_t swizzle_raw(uint8_t raw) {
return (raw & 0x1F) | // Keep the bottom 5 bits the same.
((raw >> 2) & 0x20) | // Shift the DFU bit down to bit 6.
((raw << 1) & 0xC0); // Shift the encoder bits up to be 7 & 8.
}
static bool switches_update(const uint8_t raw) {
// TODO: Only fire event on press, not release?
bool switch_changed = false;
for (size_t i = 0; i < switch_debounce.size(); i++) {
switch_changed |= switch_debounce[i].feed((raw >> i) & 1);
for (size_t i = 0; i < switch_debounce.size(); ++i) {
uint8_t bit = (raw >> i) & 0x01;
switch_changed |= switch_debounce[i].feed(bit);
}
return switch_changed;
}
static bool encoder_update(const uint8_t raw) {
bool encoder_changed = false;
encoder_changed |= encoder_debounce[0].feed((raw >> 6) & 0x01);
encoder_changed |= encoder_debounce[1].feed((raw >> 7) & 0x01);
return encoder_changed;
}
static bool encoder_read() {
const auto delta = encoder.update(
switch_debounce[5].state(),
switch_debounce[6].state());
encoder_debounce[0].state(),
encoder_debounce[1].state());
if (delta != 0) {
encoder_position += delta;
return true;
;
} else {
return false;
}
@@ -149,11 +182,13 @@ static bool encoder_read() {
void timer0_callback(GPTDriver* const) {
eventmask_t event_mask = 0;
if (touch_update()) event_mask |= EVT_MASK_TOUCH;
switches_raw = portapack::io.io_update(touch_pins_configs[touch_phase]);
if (switches_update(switches_raw)) {
switches_raw = swizzle_raw(io.io_update(touch_pins_configs[touch_phase]));
if (switches_update(switches_raw))
event_mask |= EVT_MASK_SWITCHES;
if (encoder_read()) event_mask |= EVT_MASK_ENCODER;
}
if (encoder_update(switches_raw) && encoder_read())
event_mask |= EVT_MASK_ENCODER;
/* Signal event loop */
if (event_mask) {
@@ -189,37 +224,40 @@ void controls_init() {
gptStartContinuous(&GPTD1, timer0_match_count);
// Enable repeat for directional switches only
for (size_t i = 0; i < 4; i++)
switch_debounce[i].enable_repeat();
for (auto i = Switch::Right; i <= Switch::Up; incr(i))
switch_debounce[toUType(i)].enable_repeat();
}
// Note: Called by event handler or apps, not in ISR, so some presses might be missed during high CPU utilization
SwitchesState get_switches_state() {
SwitchesState result;
// Right, Left, Down, Up, & Select switches
for (size_t i = 0; i < result.size() - 1; i++) {
// TODO: Ignore multiple keys at the same time?
// TODO: Ignore multiple keys at the same time?
for (size_t i = 0; i < result.size(); i++)
result[i] = switch_debounce[i].state();
}
// Grab Dfu switch from bit 7 and return in bit 5 so that all switches are grouped together in this 6-bit result
result[(size_t)Switch::Dfu] = switch_debounce[7].state();
return result;
}
// Configure which switches support long press (note those switches will not support Repeat function)
void switches_long_press_enable(SwitchesState switches_long_press_enabled) {
// Right, Left, Down, Up, & Select switches
for (size_t i = 0; i < switches_long_press_enabled.size() - 1; i++) {
switch_debounce[i].set_long_press_support(switches_long_press_enabled[i]);
}
/* Gets the long press enabled state for all the switches. */
SwitchesState get_switches_long_press_config() {
SwitchesState result;
// Dfu switch
switch_debounce[7].set_long_press_support(switches_long_press_enabled[(size_t)Switch::Dfu]);
for (size_t i = 0; i < result.size(); i++)
result[i] = switch_debounce[i].get_long_press_enabled();
return result;
}
bool switch_long_press_occurred(size_t v) {
return (v == (size_t)Switch::Dfu) ? switch_debounce[7].long_press_occurred() : switch_debounce[v].long_press_occurred();
/* Configures which switches support long press.
* NB: those switches will not support Repeat function. */
void set_switches_long_press_config(SwitchesState switch_config) {
for (size_t i = 0; i < switch_config.size(); i++)
switch_debounce[i].set_long_press_enabled(switch_config[i]);
}
bool switch_is_long_pressed(Switch s) {
return switch_debounce[toUType(s)].long_press_occurred();
}
EncoderPosition get_encoder_position() {
+8 -5
View File
@@ -28,25 +28,28 @@
#include "touch.hpp"
// Must be same values as in ui::KeyEvent
enum class Switch {
// TODO: Why not just reuse one or the other?
enum class Switch : uint8_t {
Right = 0,
Left = 1,
Down = 2,
Up = 3,
Sel = 4,
Dfu = 5,
Dfu = 5
};
// Index with the Switch enum.
using SwitchesState = std::bitset<6>;
using EncoderPosition = uint32_t;
void controls_init();
SwitchesState get_switches_state();
EncoderPosition get_encoder_position();
touch::Frame get_touch_frame();
void switches_long_press_enable(SwitchesState v);
bool switch_long_press_occurred(size_t v);
SwitchesState get_switches_long_press_config();
void set_switches_long_press_config(SwitchesState switch_config);
bool switch_is_long_pressed(Switch s);
namespace control {
namespace debug {
+24
View File
@@ -32,6 +32,7 @@ using namespace portapack;
#include "string_format.hpp"
#include "complex.hpp"
#include "ui_styles.hpp"
#include "ui_font_fixed_5x8.hpp"
namespace ui {
@@ -243,6 +244,7 @@ void GeoMap::paint(Painter& painter) {
// Draw the other markers
draw_markers(painter);
draw_scale(painter);
markerListUpdated = false;
set_clean();
}
@@ -283,6 +285,10 @@ void GeoMap::move(const float lon, const float lat) {
x_pos = map_width - map_rect.width();
if (y_pos > (map_height + map_rect.height()))
y_pos = map_height - map_rect.height();
// Scale calculation
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
pixels_per_km = (map_rect.width() / 2) / km_per_deg_lon;
}
bool GeoMap::init() {
@@ -318,6 +324,24 @@ bool GeoMap::manual_panning() {
return manual_panning_;
}
void GeoMap::draw_scale(Painter& painter) {
uint16_t km = 100;
uint16_t scale_width = km * pixels_per_km * map_zoom;
while (scale_width > screen_width / 2) {
scale_width /= 2;
km /= 2;
}
std::string km_string = to_string_dec_uint(km) + " km";
display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 4}, {scale_width, 2}}, Color::black());
display.fill_rectangle({{screen_width - 5, screen_height - 8}, {2, 6}}, Color::black());
display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 8}, {2, 6}}, Color::black());
painter.draw_string({(uint16_t)(screen_width - 25 - scale_width - km_string.length() * 5 / 2), screen_height - 10}, ui::font::fixed_5x8, Color::black(), Color::white(), km_string);
}
void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color) {
Point arrow_a, arrow_b, arrow_c;
+2
View File
@@ -179,6 +179,7 @@ class GeoMap : public Widget {
MapMarkerStored store_marker(GeoMarker& marker);
private:
void draw_scale(Painter& painter);
void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color);
void draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color = Color::red(), const Color fontColor = Color::white(), const Color backColor = Color::black());
void draw_markers(Painter& painter);
@@ -199,6 +200,7 @@ class GeoMap : public Widget {
int32_t prev_x_pos{0xFFFF}, prev_y_pos{0xFFFF};
float lat_{};
float lon_{};
float pixels_per_km{};
uint16_t angle_{};
std::string tag_{};
+125 -29
View File
@@ -20,16 +20,15 @@
* Boston, MA 02110-1301, USA.
*/
#include "irq_controls.hpp"
#include "max283x.hpp"
#include "portapack.hpp"
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_freqman.hpp"
#include "portapack.hpp"
using namespace portapack;
#include "string_format.hpp"
#include "max283x.hpp"
namespace ui {
/* FrequencyField ********************************************************/
@@ -38,15 +37,26 @@ FrequencyField::FrequencyField(
const Point parent_pos)
: Widget{{parent_pos, {8 * 10, 16}}},
length_{11},
range(rf::tuning_range) {
range_{rf::tuning_range} {
initial_switch_config_ = get_switches_long_press_config();
set_focusable(true);
}
FrequencyField::~FrequencyField() {
reset_switch_config();
}
rf::Frequency FrequencyField::value() const {
return value_;
}
void FrequencyField::set_value(rf::Frequency new_value) {
// While in digit mode, don't update if the value is
// out of range. That way going out of range doesn't
// cause all the other digits to reset.
if (digit_mode_ && !range_.contains_inc(new_value))
return;
new_value = clamp_value(new_value);
if (new_value != value_) {
@@ -59,48 +69,98 @@ void FrequencyField::set_value(rf::Frequency new_value) {
}
void FrequencyField::set_step(rf::Frequency new_value) {
step = new_value;
// TODO: Quantize current frequency to a step of the new size?
step_ = new_value;
}
void FrequencyField::set_allow_digit_mode(bool allowed) {
allow_digit_mode_ = allowed;
if (!allowed && digit_mode_) {
digit_mode_ = false;
reset_switch_config();
set_dirty();
}
}
void FrequencyField::paint(Painter& painter) {
const std::string str_value = to_string_short_freq(value_);
const auto str_value = to_string_short_freq(value_);
const auto paint_style = has_focus() ? style().invert() : style();
painter.draw_string(
screen_pos(),
paint_style,
str_value);
// Highlight current digit in digit_mode.
if (digit_mode_) {
auto p = screen_pos();
p += {digit_ * char_width, 0};
painter.draw_char(p, Styles::bg_blue, str_value[digit_]);
}
}
bool FrequencyField::on_key(const ui::KeyEvent event) {
if (event == ui::KeyEvent::Select) {
bool FrequencyField::on_key(KeyEvent event) {
if (event == KeyEvent::Select) {
// Toggle 'digit' mode with long-press.
if (digit_mode_ || key_is_long_pressed(event)) {
digit_mode_ = !digit_mode_;
set_dirty();
return true;
}
if (on_edit) {
on_edit();
return true;
}
}
if (digit_mode_) {
constexpr uint8_t decimal_pos = 4;
int8_t delta = 0;
switch (event) {
case KeyEvent::Up:
set_value(value_ + digit_step());
break;
case KeyEvent::Down:
set_value(value_ - digit_step());
break;
case KeyEvent::Left:
delta = -1;
break;
case KeyEvent::Right:
delta = 1;
break;
default:
return false;
}
if (delta != 0) {
digit_ += delta;
// If on decimal, skip it.
if (digit_ == decimal_pos)
digit_ += delta;
// Otherwise ensure in bounds.
else
digit_ = clip<int8_t>(digit_, 0, 8);
set_dirty();
}
return true;
}
return false;
}
bool FrequencyField::on_encoder(const EncoderEvent delta) {
if (step == 0) { // 'Auto' mode.'
auto ms = RTT2MS(halGetCounterValue());
auto delta_ms = last_ms_ <= ms ? ms - last_ms_ : ms;
last_ms_ = ms;
if (digit_mode_)
set_value(value_ + (delta * digit_step()));
else
set_value(value_ + (delta * step_));
// The goal is to map 'scale' to a range of about 10 to 10M.
// The faster the encoder is rotated, the larger the step.
// Linear doesn't feel right. Hyperbolic felt better.
// To get these magic numbers, I graphed the function until the
// curve shape seemed about right then tested on device.
delta_ms = std::min(145ull, delta_ms) + 5; // Prevent DIV/0
int64_t scale = 200'000'000 / (0.001'55 * std::pow(delta_ms, 5.45)) + 8;
set_value(value() + (delta * scale));
} else {
set_value(value() + (delta * step));
}
return true;
}
@@ -112,13 +172,49 @@ bool FrequencyField::on_touch(const TouchEvent event) {
}
void FrequencyField::on_focus() {
if (on_show_options) {
if (on_show_options)
on_show_options();
}
enable_switch_config();
}
void FrequencyField::on_blur() {
reset_switch_config();
}
rf::Frequency FrequencyField::digit_step() const {
constexpr rf::Frequency steps[] = {
1'000'000'000,
100'000'000,
10'000'000,
1'000'000,
0, // Decimal point position.
100'000,
10'000,
1'000,
100,
};
return digit_ < std::size(steps) ? steps[digit_] : 0;
}
rf::Frequency FrequencyField::clamp_value(rf::Frequency value) {
return range.clip(value);
return range_.clip(value);
}
void FrequencyField::enable_switch_config() {
// Don't enable long press testing when not allowed.
if (!allow_digit_mode_)
return;
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void FrequencyField::reset_switch_config() {
set_switches_long_press_config(initial_switch_config_);
}
/* FrequencyKeypadView ***************************************************/
+18 -4
View File
@@ -27,6 +27,7 @@
#include "ui_painter.hpp"
#include "ui_widget.hpp"
#include "irq_controls.hpp"
#include "rf_path.hpp"
#include <cstddef>
@@ -45,27 +46,41 @@ class FrequencyField : public Widget {
using range_t = rf::FrequencyRange;
FrequencyField(Point parent_pos);
~FrequencyField();
rf::Frequency value() const;
void set_value(rf::Frequency new_value);
void set_step(rf::Frequency new_value);
void set_allow_digit_mode(bool allowed);
void paint(Painter& painter) override;
bool on_key(ui::KeyEvent event) override;
bool on_key(KeyEvent event) override;
bool on_encoder(EncoderEvent delta) override;
bool on_touch(TouchEvent event) override;
void on_focus() override;
void on_blur() override;
private:
const size_t length_;
const range_t range;
const range_t range_;
rf::Frequency value_{0};
rf::Frequency step{25000};
rf::Frequency step_{25000};
uint64_t last_ms_{0};
uint8_t digit_{3};
bool digit_mode_{false};
bool allow_digit_mode_{true};
SwitchesState initial_switch_config_{};
/* Gets the step value for the given digit when in digit_mode. */
rf::Frequency digit_step() const;
rf::Frequency clamp_value(rf::Frequency value);
void enable_switch_config();
void reset_switch_config();
};
template <size_t N>
@@ -242,7 +257,6 @@ class FrequencyStepView : public OptionsField {
parent_pos,
5,
{
{" Auto", 0}, /* Faster == larger step. */
{" 10", 10}, /* Fine tuning SSB voice pitch,in HF and QO-100 sat #669 */
{" 50", 50}, /* added 50Hz/10Hz,but we do not increase GUI digit decimal */
{" 100", 100},
+3 -3
View File
@@ -100,6 +100,9 @@ class ILI9341 {
constexpr ui::Dim height() const { return 320; }
constexpr ui::Rect screen_rect() const { return {0, 0, width(), height()}; }
void draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count);
void read_pixels(const ui::Rect r, ui::ColorRGB888* const colors, const size_t count);
private:
struct scroll_t {
ui::Coord top_area;
@@ -109,9 +112,6 @@ class ILI9341 {
};
scroll_t scroll_state;
void draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count);
void read_pixels(const ui::Rect r, ui::ColorRGB888* const colors, const size_t count);
};
} /* namespace lcd */
@@ -80,7 +80,7 @@ constexpr clkout_freq_range_t clkout_freq_range{10, 60000};
constexpr uint16_t clkout_freq_reset_value{10000};
enum data_structure_version_enum : uint32_t {
VERSION_CURRENT = 0x10000004,
VERSION_CURRENT = 0x10000005,
};
static const uint32_t TOUCH_CALIBRATION_MAGIC = 0x074af82f;
@@ -107,11 +107,9 @@ struct backlight_config_t {
};
enum encoder_dial_sensitivity {
DIAL_SENSITIVITY_HIGHEST = 0,
DIAL_SENSITIVITY_HIGH = 1,
DIAL_SENSITIVITY_NORMAL = 2,
DIAL_SENSITIVITY_LOW = 3,
DIAL_SENSITIVITY_LOWEST = 4,
DIAL_SENSITIVITY_NORMAL = 0,
DIAL_SENSITIVITY_LOW = 1,
DIAL_SENSITIVITY_HIGH = 2,
NUM_DIAL_SENSITIVITY
};
+11
View File
@@ -20,6 +20,7 @@
*/
#include "ui.hpp"
#include "irq_controls.hpp"
#include "sine_table.hpp"
#include "utility.hpp"
@@ -102,4 +103,14 @@ Point fast_polar_to_point(int32_t angle, uint32_t distance) {
(int16_sin_s4(((1 << 16) * (-angle - 90)) / 360) * distance) / (1 << 16));
}
bool key_is_long_pressed(KeyEvent key) {
if (key < KeyEvent::Back) {
// TODO: this would make more sense as a flag on KeyEvent
// passed up to the UI via event dispatch.
return switch_is_long_pressed(static_cast<Switch>(key));
}
return false;
}
} /* namespace ui */
+10 -1
View File
@@ -34,6 +34,10 @@ using Dim = int16_t;
constexpr uint16_t screen_width = 240;
constexpr uint16_t screen_height = 320;
/* Dimensions for the default font character. */
constexpr uint16_t char_width = 8;
constexpr uint16_t char_height = 16;
struct Color {
uint16_t v; // rrrrrGGGGGGbbbbb
@@ -328,7 +332,7 @@ struct Bitmap {
const uint8_t* const data;
};
enum class KeyEvent {
enum class KeyEvent : uint8_t {
/* Ordinals map to bit positions reported by CPLD */
Right = 0,
Left = 1,
@@ -356,6 +360,11 @@ Point polar_to_point(float angle, uint32_t distance);
Point fast_polar_to_point(int32_t angle, uint32_t distance);
/* Default font glyph size. */
constexpr Size char_size{char_width, char_height};
bool key_is_long_pressed(KeyEvent key);
} /* namespace ui */
#endif /*__UI_H__*/
+13 -2
View File
@@ -53,6 +53,12 @@ constexpr typename std::underlying_type<E>::type toUType(E enumerator) noexcept
return static_cast<typename std::underlying_type<E>::type>(enumerator);
}
/* Increments an enum value. Enumerator values are assumed to be serial. */
template <typename E>
void incr(E& e) {
e = static_cast<E>(toUType(e) + 1);
}
inline uint32_t flp2(uint32_t v) {
v |= v >> 1;
v |= v >> 2;
@@ -180,13 +186,18 @@ struct range_t {
return value < minimum;
}
/* Exclusive of maximum. */
constexpr bool contains(const T& value) const {
// TODO: Subtle gotcha here! Range test doesn't include maximum!
return (value >= minimum) && (value < maximum);
}
/* Inclusive of maximum. */
constexpr bool contains_inc(const T& value) const {
return (value >= minimum) && (value <= maximum);
}
/* Exclusive of maximum. */
constexpr bool out_of_range(const T& value) const {
// TODO: Subtle gotcha here! Range test in contains() doesn't include maximum!
return !contains(value);
}
};
@@ -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();