mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-14 18:43:01 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ce48dbcf6 | |||
| 92072b4225 | |||
| 4e823dd3a3 | |||
| 53d9cecdaf | |||
| 680f1f49ff | |||
| 6574272ca8 | |||
| f5c4aa2be2 | |||
| 2498861003 | |||
| 25923e82a4 | |||
| 61cb57e48d |
@@ -25,10 +25,10 @@
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "capture_app.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -47,6 +48,14 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
/* THE ONE LINE BELOW IS A TEMPORARY KLUDGE WORKAROUND FOR A MYSTERY M4 BASEBAND HANG ISSUE WHEN THE CAPTURE
|
||||
APP IS THE FIRST APP STARTED AFTER POWER-UP, OR THE CAPTURE APP IS RUN AFTER RUNNING REPLAY WITH A HIGH
|
||||
SAMPLE RATE. IT SHOULD NOT BE NECESSARY SINCE sampling_rate IS ALREADY INITIALIZED BY RADIOSTATE BUT
|
||||
APPARENTLY IS ADDING JUST THE RIGHT AMOUNT OF DELAY. ISSUE DOES NOT AFFECT ALL PORTAPACK UNITS.
|
||||
INVESTIGATION IS ONGOING, SEE ISSUE #1283. */
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
/* END MYSTERY HANG WORKAROUND. */
|
||||
|
||||
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);
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "freqman.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
|
||||
@@ -26,13 +26,42 @@
|
||||
#include "event_m0.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// TODO: Clean up after moving to better lookup tables.
|
||||
using options_t = OptionsField::options_t;
|
||||
extern options_t freqman_modulations;
|
||||
extern options_t freqman_bandwidths[4];
|
||||
extern options_t freqman_steps;
|
||||
extern options_t freqman_steps_short;
|
||||
|
||||
/* Set options. */
|
||||
void freqman_set_modulation_option(OptionsField& option) {
|
||||
option.set_options(freqman_modulations);
|
||||
}
|
||||
|
||||
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
|
||||
if (is_valid(modulation))
|
||||
option.set_options(freqman_bandwidths[modulation]);
|
||||
}
|
||||
|
||||
void freqman_set_step_option(OptionsField& option) {
|
||||
option.set_options(freqman_steps);
|
||||
}
|
||||
|
||||
void freqman_set_step_option_short(OptionsField& option) {
|
||||
option.set_options(freqman_steps_short);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* FreqManBaseView ***************************************/
|
||||
@@ -147,7 +176,7 @@ FrequencySaveView::FrequencySaveView(
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
db_.insert_entry(entry_, db_.entry_count());
|
||||
db_.insert_entry(db_.entry_count(), entry_);
|
||||
nav_.pop();
|
||||
};
|
||||
}
|
||||
@@ -188,8 +217,15 @@ FrequencyLoadView::FrequencyLoadView(
|
||||
|
||||
/* FrequencyManagerView **********************************/
|
||||
|
||||
void FrequencyManagerView::on_edit_entry() {
|
||||
auto edit_view = nav_.push<FrequencyEditView>(current_entry());
|
||||
edit_view->on_save = [this](const freqman_entry& entry) {
|
||||
db_.replace_entry(current_index(), entry);
|
||||
freqlist_view.set_dirty();
|
||||
};
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_edit_freq() {
|
||||
// TODO: range edit support.
|
||||
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
|
||||
freq_edit_view->on_changed = [this](rf::Frequency f) {
|
||||
auto entry = current_entry();
|
||||
@@ -240,7 +276,7 @@ void FrequencyManagerView::on_add_entry() {
|
||||
};
|
||||
|
||||
// Add will insert below the currently selected item.
|
||||
db_.insert_entry(entry, current_index() + 1);
|
||||
db_.insert_entry(current_index() + 1, entry);
|
||||
refresh_list(1);
|
||||
}
|
||||
|
||||
@@ -263,20 +299,21 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
: FreqManBaseView(nav) {
|
||||
add_children(
|
||||
{&freqlist_view,
|
||||
&labels,
|
||||
&button_add_category,
|
||||
&button_del_category,
|
||||
&button_edit_entry,
|
||||
&rect_padding,
|
||||
&button_edit_freq,
|
||||
&button_edit_desc,
|
||||
&button_add_entry,
|
||||
&button_del_entry});
|
||||
|
||||
freqlist_view.on_select = [this](size_t) {
|
||||
button_edit_freq.focus();
|
||||
button_edit_entry.focus();
|
||||
};
|
||||
// Allows for quickly exiting control.
|
||||
freqlist_view.on_leave = [this]() {
|
||||
button_edit_freq.focus();
|
||||
button_edit_entry.focus();
|
||||
};
|
||||
|
||||
button_add_category.on_select = [this]() {
|
||||
@@ -287,6 +324,10 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
on_del_category();
|
||||
};
|
||||
|
||||
button_edit_entry.on_select = [this](Button&) {
|
||||
on_edit_entry();
|
||||
};
|
||||
|
||||
button_edit_freq.on_select = [this](Button&) {
|
||||
on_edit_freq();
|
||||
};
|
||||
@@ -304,4 +345,180 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
};
|
||||
}
|
||||
|
||||
/* FrequencyEditView *************************************/
|
||||
|
||||
FrequencyEditView::FrequencyEditView(
|
||||
NavigationView& nav,
|
||||
freqman_entry entry)
|
||||
: nav_{nav},
|
||||
entry_{std::move(entry)} {
|
||||
add_children({&labels,
|
||||
&field_type,
|
||||
&field_freq_a,
|
||||
&field_freq_b,
|
||||
&field_modulation,
|
||||
&field_bandwidth,
|
||||
&field_step,
|
||||
&field_tone,
|
||||
&field_description,
|
||||
&text_validation,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
freqman_set_modulation_option(field_modulation);
|
||||
populate_bandwidth_options();
|
||||
populate_step_options();
|
||||
populate_tone_options();
|
||||
|
||||
// Add the "invalid/unset" option. Kind of hacky, but...
|
||||
field_modulation.options().insert(
|
||||
field_modulation.options().begin(), {"None", -1});
|
||||
field_step.options().insert(
|
||||
field_step.options().begin(), {"None", -1});
|
||||
|
||||
field_type.set_by_value((int32_t)entry_.type);
|
||||
field_type.on_change = [this](size_t, auto value) {
|
||||
entry_.type = static_cast<freqman_type>(value);
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
// TODO: this pattern should be able to be wrapped up.
|
||||
field_freq_a.set_value(entry_.frequency_a);
|
||||
field_freq_a.on_change = [this](rf::Frequency f) {
|
||||
entry_.frequency_a = f;
|
||||
refresh_ui();
|
||||
};
|
||||
field_freq_a.on_edit = [this]() {
|
||||
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_a.value());
|
||||
freq_view->on_changed = [this](rf::Frequency f) {
|
||||
field_freq_a.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_freq_b.set_value(entry_.frequency_b);
|
||||
field_freq_b.on_change = [this](rf::Frequency f) {
|
||||
entry_.frequency_b = f;
|
||||
refresh_ui();
|
||||
};
|
||||
field_freq_b.on_edit = [this]() {
|
||||
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_b.value());
|
||||
freq_view->on_changed = [this](rf::Frequency f) {
|
||||
field_freq_b.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_modulation.set_by_value((int32_t)entry_.modulation);
|
||||
field_modulation.on_change = [this](size_t, auto value) {
|
||||
entry_.modulation = static_cast<freqman_index_t>(value);
|
||||
populate_bandwidth_options();
|
||||
};
|
||||
|
||||
field_bandwidth.set_by_value((int32_t)entry_.bandwidth);
|
||||
field_bandwidth.on_change = [this](size_t, auto value) {
|
||||
entry_.bandwidth = static_cast<freqman_index_t>(value);
|
||||
};
|
||||
|
||||
field_step.set_by_value((int32_t)entry_.step);
|
||||
field_step.on_change = [this](size_t, auto value) {
|
||||
entry_.step = static_cast<freqman_index_t>(value);
|
||||
};
|
||||
|
||||
field_tone.set_by_value((int32_t)entry_.tone);
|
||||
field_tone.on_change = [this](size_t, auto value) {
|
||||
entry_.tone = static_cast<freqman_index_t>(value);
|
||||
};
|
||||
|
||||
field_description.set_text(entry_.description);
|
||||
field_description.on_change = [this](TextField& tf) {
|
||||
entry_.description = tf.get_text();
|
||||
};
|
||||
field_description.on_select = [this](TextField& tf) {
|
||||
temp_buffer_ = tf.get_text();
|
||||
text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max,
|
||||
[this, &tf](std::string& new_desc) {
|
||||
tf.set_text(new_desc);
|
||||
});
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
if (on_save)
|
||||
on_save(std::move(entry_));
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [this](Button&) {
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void FrequencyEditView::focus() {
|
||||
button_cancel.focus();
|
||||
}
|
||||
|
||||
void FrequencyEditView::refresh_ui() {
|
||||
// This needs to be called whenever a UI option is changed
|
||||
// in a way that causes fields to be unused or would make the
|
||||
// entry fail validation.
|
||||
|
||||
auto is_range = entry_.type == freqman_type::Range;
|
||||
auto is_ham = entry_.type == freqman_type::HamRadio;
|
||||
auto has_freq_b = is_range || is_ham;
|
||||
|
||||
field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey);
|
||||
field_step.set_style(is_range ? &Styles::white : &Styles::grey);
|
||||
field_tone.set_style(is_ham ? &Styles::white : &Styles::grey);
|
||||
|
||||
if (is_valid(entry_)) {
|
||||
text_validation.set("Valid");
|
||||
text_validation.set_style(&Styles::green);
|
||||
} else {
|
||||
text_validation.set("Error");
|
||||
text_validation.set_style(&Styles::red);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: The following functions shouldn't be needed once
|
||||
// freqman_db lookup tables are complete.
|
||||
void FrequencyEditView::populate_bandwidth_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
if (entry_.modulation < std::size(freqman_bandwidths)) {
|
||||
auto& bandwidths = freqman_bandwidths[entry_.modulation];
|
||||
for (auto i = 1u; i < bandwidths.size(); ++i) {
|
||||
auto& item = bandwidths[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
}
|
||||
|
||||
field_bandwidth.set_options(std::move(options));
|
||||
}
|
||||
|
||||
void FrequencyEditView::populate_step_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 1u; i < freqman_steps.size(); ++i) {
|
||||
auto& item = freqman_steps[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
|
||||
field_step.set_options(std::move(options));
|
||||
}
|
||||
|
||||
void FrequencyEditView::populate_tone_options() {
|
||||
using namespace tonekey;
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 1u; i < tone_keys.size(); ++i) {
|
||||
auto& item = tone_keys[i];
|
||||
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
|
||||
}
|
||||
|
||||
field_tone.set_options(std::move(options));
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -36,17 +36,18 @@ namespace ui {
|
||||
|
||||
class FreqManBaseView : public View {
|
||||
public:
|
||||
FreqManBaseView(
|
||||
NavigationView& nav);
|
||||
FreqManBaseView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
static constexpr size_t desc_edit_max = 0x80;
|
||||
|
||||
protected:
|
||||
using options_t = OptionsField::options_t;
|
||||
|
||||
NavigationView& nav_;
|
||||
freqman_error error_{NO_ERROR};
|
||||
std::function<void(void)> on_select_frequency{nullptr};
|
||||
std::function<void(void)> on_select_frequency{};
|
||||
|
||||
void change_category(size_t new_index);
|
||||
/* Access the categories directly from the OptionsField.
|
||||
@@ -79,14 +80,13 @@ class FreqManBaseView : public View {
|
||||
protected:
|
||||
/* Static so selected category is persisted across UI instances. */
|
||||
static size_t current_category_index;
|
||||
static constexpr size_t desc_edit_max = 0x80;
|
||||
};
|
||||
|
||||
// TODO: support for new category.
|
||||
class FrequencySaveView : public FreqManBaseView {
|
||||
public:
|
||||
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
|
||||
std::string title() const override { return "Save freq"; };
|
||||
std::string title() const override { return "Save freq"; }
|
||||
|
||||
private:
|
||||
std::string temp_buffer_{};
|
||||
@@ -122,17 +122,18 @@ class FrequencyLoadView : public FreqManBaseView {
|
||||
std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{};
|
||||
|
||||
FrequencyLoadView(NavigationView& nav);
|
||||
std::string title() const override { return "Load freq"; };
|
||||
std::string title() const override { return "Load freq"; }
|
||||
};
|
||||
|
||||
class FrequencyManagerView : public FreqManBaseView {
|
||||
public:
|
||||
FrequencyManagerView(NavigationView& nav);
|
||||
std::string title() const override { return "Freqman"; };
|
||||
std::string title() const override { return "Freqman"; }
|
||||
|
||||
private:
|
||||
std::string temp_buffer_{};
|
||||
|
||||
void on_edit_entry();
|
||||
void on_edit_freq();
|
||||
void on_edit_desc();
|
||||
void on_add_category();
|
||||
@@ -140,9 +141,6 @@ class FrequencyManagerView : public FreqManBaseView {
|
||||
void on_add_entry();
|
||||
void on_del_entry();
|
||||
|
||||
Labels labels{
|
||||
{{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}};
|
||||
|
||||
NewButton button_add_category{
|
||||
{23 * 8, 0 * 16, 7 * 4, 20},
|
||||
{},
|
||||
@@ -157,6 +155,14 @@ class FrequencyManagerView : public FreqManBaseView {
|
||||
Color::red(),
|
||||
true};
|
||||
|
||||
Button button_edit_entry{
|
||||
{0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
|
||||
"Edit"};
|
||||
|
||||
Rectangle rect_padding{
|
||||
{15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
|
||||
Color::grey()};
|
||||
|
||||
Button button_edit_freq{
|
||||
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
|
||||
"Frequency"};
|
||||
@@ -180,4 +186,79 @@ class FrequencyManagerView : public FreqManBaseView {
|
||||
true};
|
||||
};
|
||||
|
||||
class FrequencyEditView : public View {
|
||||
public:
|
||||
std::function<void(const freqman_entry&)> on_save{};
|
||||
|
||||
FrequencyEditView(
|
||||
NavigationView& nav,
|
||||
freqman_entry entry);
|
||||
std::string title() const override { return "Freqman Edit"; }
|
||||
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
freqman_entry entry_;
|
||||
std::string temp_buffer_{};
|
||||
|
||||
void refresh_ui();
|
||||
void populate_bandwidth_options();
|
||||
void populate_step_options();
|
||||
void populate_tone_options();
|
||||
|
||||
Labels labels{
|
||||
{{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()},
|
||||
{{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()},
|
||||
{{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()},
|
||||
{{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()},
|
||||
{{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()},
|
||||
{{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()},
|
||||
{{0 * 8, 8 * 16}, "Step :", Color::light_grey()},
|
||||
{{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()},
|
||||
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_type{{13 * 8, 3 * 16}, 8, {
|
||||
{"Single", 0},
|
||||
{"Range", 1},
|
||||
{"HamRadio", 2},
|
||||
{"Raw", 3},
|
||||
}};
|
||||
|
||||
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
|
||||
|
||||
FrequencyField field_freq_b{{13 * 8, 5 * 16}};
|
||||
|
||||
OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}};
|
||||
|
||||
OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}};
|
||||
|
||||
OptionsField field_step{{13 * 8, 8 * 16}, 12, {}};
|
||||
|
||||
OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}};
|
||||
|
||||
TextField field_description{
|
||||
{13 * 8, 10 * 16, 17 * 8, 1 * 16},
|
||||
{}};
|
||||
|
||||
Text text_validation{
|
||||
{12 * 8, 12 * 16, 5 * 8, 1 * 16},
|
||||
{}};
|
||||
|
||||
Button button_save{
|
||||
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Cancel"};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
|
||||
void freqman_set_modulation_option(ui::OptionsField& option);
|
||||
void freqman_set_step_option(ui::OptionsField& option);
|
||||
void freqman_set_step_option_short(ui::OptionsField& option);
|
||||
void freqman_set_tone_option(ui::OptionsField& option);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "ui_level.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -24,21 +24,21 @@
|
||||
#ifndef _UI_LEVEL
|
||||
#define _UI_LEVEL
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "receiver_model.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 "ui_spectrum.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.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_spectrum.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "portapack_hal.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "tonesets.hpp"
|
||||
@@ -355,13 +356,10 @@ MicTXView::MicTXView(
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.hidden(0); // we are in FM mode, we should have active the Key-tones & CTCSS option.
|
||||
|
||||
rxbw.emplace_back(" NFM1:8k5 ", 0); // restore the original dynamic field_rxbw value.
|
||||
rxbw.emplace_back(" NFM2:11k ", 1);
|
||||
rxbw.emplace_back(" FM :16k ", 2);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
|
||||
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
|
||||
freqman_set_bandwidth_option(NFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with NFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(2); // // bw 16k (2) default
|
||||
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
|
||||
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
|
||||
break;
|
||||
case 1: //{ "WFM", 1 }
|
||||
enable_am = false;
|
||||
@@ -375,13 +373,10 @@ MicTXView::MicTXView(
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.hidden(0); // we are in WFM mode, we should have active the Key-tones & CTCSS option.
|
||||
|
||||
rxbw.emplace_back(" 200k-WFM ", 0); // We allow the user selection of the 3 x WFM BW filters, (0) WFM-200K, (1) WFM-180K, (2) WFM-40K.
|
||||
rxbw.emplace_back(" 180k-WFM ", 1);
|
||||
rxbw.emplace_back(" 40k-WFM ", 2);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
|
||||
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
|
||||
freqman_set_bandwidth_option(WFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with WFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(0); // bw 200k (0) default
|
||||
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
|
||||
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
|
||||
break;
|
||||
case 2: //{ "AM", 2 }
|
||||
enable_am = true;
|
||||
@@ -390,8 +385,8 @@ MicTXView::MicTXView(
|
||||
set_dirty(); // Refresh display
|
||||
options_tone_key.hidden(1); // we hide that Key-tones & CTCSS input selecction, (no meaning in AM/DSB/SSB).
|
||||
|
||||
rxbw.emplace_back(" DSB1-9k ", 0); // we offer in AM DSB two audio BW 9k / 6k.
|
||||
rxbw.emplace_back(" DSB2-6k ", 1);
|
||||
rxbw.emplace_back("DSB 9k", 0); // we offer in AM DSB two audio BW 9k / 6k.
|
||||
rxbw.emplace_back("DSB 6k", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we show fixed RX AM BW 6Khz
|
||||
@@ -404,8 +399,8 @@ MicTXView::MicTXView(
|
||||
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
|
||||
check_rogerbeep.hidden(1); // hide that roger beep selection.
|
||||
|
||||
rxbw.emplace_back(" USB+3k ", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
rxbw.emplace_back("USB+3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
set_dirty(); // Refresh display
|
||||
break;
|
||||
@@ -415,8 +410,8 @@ MicTXView::MicTXView(
|
||||
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
|
||||
check_rogerbeep.hidden(1); // hide that roger beep selection.
|
||||
|
||||
rxbw.emplace_back(" LSB-3k ", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
rxbw.emplace_back("LSB-3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
set_dirty(); // Refresh display
|
||||
break;
|
||||
@@ -425,8 +420,8 @@ MicTXView::MicTXView(
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
|
||||
|
||||
rxbw.emplace_back("SSB1:USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-C case to the field_rxbw value.
|
||||
rxbw.emplace_back("SSB2:LSB-3k", 1);
|
||||
rxbw.emplace_back("USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-SC case to the field_rxbw value.
|
||||
rxbw.emplace_back("LSB-3k", 1);
|
||||
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField options_ak4951_alc_mode{
|
||||
{20 * 8, 1 * 8},
|
||||
11,
|
||||
10, // Label has 10 chars
|
||||
{
|
||||
{" OFF-12kHz", 0}, // Nothing changed from ORIGINAL, keeping ALL programmable AK4951 Digital Block->OFF, sampling 24Khz)
|
||||
{"+12dB-6kHz", 1}, // ALC-> on, (+12dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
@@ -198,7 +198,7 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField options_wm8731_boost_mode{
|
||||
{22 * 8, 1 * 8},
|
||||
5,
|
||||
8, // Label has 8 chars
|
||||
{
|
||||
{"ON +12dB", 0}, // WM8731 Mic Boost ON, original+12dBs condition, easy to saturate ADC sat in high voice, relative G = +12 dB's respect ref level
|
||||
{"ON +06dB", 1}, // WM8731 Mic Boost ON, original+6 dBs condition, easy to saturate ADC sat in high voice, relative G = +06 dB's respect ref level
|
||||
@@ -298,11 +298,11 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField field_rxbw{
|
||||
{19 * 8, (23 * 8) + 2},
|
||||
3,
|
||||
7,
|
||||
{
|
||||
{" NFM1:8k5 ", 0},
|
||||
{" NFM2:11k ", 1},
|
||||
{" FM :16k ", 2},
|
||||
{" 8k5 ", 0}, // Initial dynamic values when we start Mic App.
|
||||
{" 11k ", 1},
|
||||
{" 16k ", 2},
|
||||
}};
|
||||
|
||||
NumberField field_squelch{
|
||||
|
||||
@@ -23,13 +23,17 @@
|
||||
|
||||
#include "ui_recon.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "capture_app.hpp"
|
||||
#include "convert.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "tone_key.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
using portapack::memory::map::backup_ram;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -124,152 +128,81 @@ void ReconView::colorize_waits() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ReconView::recon_save_freq(const std::string& freq_file_path, size_t freq_index, bool warn_if_exists) {
|
||||
File recon_file;
|
||||
|
||||
bool ReconView::recon_save_freq(const fs::path& path, size_t freq_index, bool warn_if_exists) {
|
||||
if (frequency_list.size() == 0 || !current_is_valid())
|
||||
return false;
|
||||
|
||||
FreqmanDB freq_db;
|
||||
if (!freq_db.open(path, /*create*/ true))
|
||||
return false;
|
||||
|
||||
freqman_entry entry = *frequency_list[freq_index]; // Makes a copy.
|
||||
entry.frequency_a = freq;
|
||||
entry.frequency_b = 0;
|
||||
entry.modulation = last_entry.modulation;
|
||||
entry.bandwidth = last_entry.bandwidth;
|
||||
entry.type = freqman_type::Single;
|
||||
|
||||
// TODO: Use FreqmanDB
|
||||
auto frequency_to_add = to_freqman_string(entry);
|
||||
|
||||
auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt
|
||||
if (!result.is_valid()) {
|
||||
char one_char[1]{}; // Read it char by char
|
||||
std::string line{}; // and put read line in here
|
||||
bool found{false};
|
||||
for (size_t pointer = 0; pointer < recon_file.size(); pointer++) {
|
||||
recon_file.seek(pointer);
|
||||
recon_file.read(one_char, 1);
|
||||
if ((int)one_char[0] > 31) { // ascii space upwards
|
||||
line += one_char[0]; // add it to the textline
|
||||
} else if (one_char[0] == '\n') { // New Line
|
||||
if (line.compare(0, frequency_to_add.size(), frequency_to_add) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
line.clear(); // Ready for next textline
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
result = recon_file.append(freq_file_path); // Second: append if it is not there
|
||||
if (!result.is_valid()) {
|
||||
recon_file.write_line(frequency_to_add);
|
||||
}
|
||||
}
|
||||
if (found && warn_if_exists) {
|
||||
nav_.display_modal("Error", "Frequency already exists");
|
||||
}
|
||||
} else {
|
||||
result = recon_file.create(freq_file_path); // First freq if no output file
|
||||
if (!result.is_valid()) {
|
||||
recon_file.write_line(frequency_to_add);
|
||||
}
|
||||
// For ranges, save the current frequency instead.
|
||||
if (entry.type == freqman_type::Range) {
|
||||
entry.frequency_a = freq;
|
||||
entry.frequency_b = 0;
|
||||
entry.type = freqman_type::Single;
|
||||
entry.modulation = last_entry.modulation;
|
||||
entry.bandwidth = last_entry.bandwidth;
|
||||
entry.step = freqman_invalid_index;
|
||||
}
|
||||
|
||||
auto it = freq_db.find_entry(entry);
|
||||
auto found = (it != freq_db.end());
|
||||
|
||||
if (found && warn_if_exists)
|
||||
nav_.display_modal("Error", "Frequency already exists");
|
||||
|
||||
if (!found)
|
||||
freq_db.append_entry(entry);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReconView::recon_load_config_from_sd() {
|
||||
File settings_file{};
|
||||
size_t length{0};
|
||||
size_t file_position{0};
|
||||
char* pos{NULL};
|
||||
char* line_start{NULL};
|
||||
char* line_end{NULL};
|
||||
char file_data[257]{};
|
||||
uint32_t it{0};
|
||||
uint32_t nb_params{RECON_SETTINGS_NB_PARAMS};
|
||||
std::string params[RECON_SETTINGS_NB_PARAMS]{};
|
||||
|
||||
make_new_directory(u"SETTINGS");
|
||||
|
||||
auto result = settings_file.open(RECON_CFG_FILE);
|
||||
if (!result.is_valid()) {
|
||||
while (it < nb_params) {
|
||||
// Read a 256 bytes block from file
|
||||
settings_file.seek(file_position);
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = settings_file.read(file_data, 256);
|
||||
if (read_size.is_error())
|
||||
break;
|
||||
file_position += 256;
|
||||
// Reset line_start to beginning of buffer
|
||||
line_start = file_data;
|
||||
pos = line_start;
|
||||
while ((line_end = strstr(line_start, "\x0A"))) {
|
||||
length = line_end - line_start - 1;
|
||||
params[it] = std::string(pos, length);
|
||||
it++;
|
||||
line_start = line_end + 1;
|
||||
pos = line_start;
|
||||
if (line_start - file_data >= 256)
|
||||
break;
|
||||
if (it >= nb_params)
|
||||
break;
|
||||
}
|
||||
if (read_size.value() != 256)
|
||||
break; // End of file
|
||||
|
||||
// Restart at beginning of last incomplete line
|
||||
file_position -= (file_data + 256 - line_start);
|
||||
}
|
||||
}
|
||||
|
||||
if (it < nb_params) {
|
||||
/* bad number of params, signal defaults */
|
||||
input_file = "RECON";
|
||||
output_file = "RECON_RESULTS";
|
||||
recon_lock_duration = RECON_MIN_LOCK_DURATION;
|
||||
recon_lock_nb_match = RECON_DEF_NB_MATCH;
|
||||
squelch = -14;
|
||||
recon_match_mode = RECON_MATCH_CONTINUOUS;
|
||||
wait = RECON_DEF_WAIT_DURATION;
|
||||
File settings_file;
|
||||
auto error = settings_file.open(RECON_CFG_FILE);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
auto complete = false;
|
||||
auto line_nb = 0;
|
||||
auto reader = FileLineReader(settings_file);
|
||||
for (const auto& line : reader) {
|
||||
switch (line_nb) {
|
||||
case 0:
|
||||
input_file = trim(line);
|
||||
break;
|
||||
case 1:
|
||||
output_file = trim(line);
|
||||
break;
|
||||
case 2:
|
||||
parse_int(line, recon_lock_duration);
|
||||
break;
|
||||
case 3:
|
||||
parse_int(line, recon_lock_nb_match);
|
||||
break;
|
||||
case 4:
|
||||
parse_int(line, squelch);
|
||||
break;
|
||||
case 5:
|
||||
parse_int(line, recon_match_mode);
|
||||
break;
|
||||
case 6:
|
||||
parse_int(line, recon_match_mode);
|
||||
complete = true; // NB: Last entry.
|
||||
break;
|
||||
}
|
||||
|
||||
if (complete) break;
|
||||
|
||||
line_nb++;
|
||||
}
|
||||
|
||||
if (it > 0)
|
||||
input_file = params[0];
|
||||
else
|
||||
input_file = "RECON";
|
||||
|
||||
if (it > 1)
|
||||
output_file = params[1];
|
||||
else
|
||||
output_file = "RECON_RESULTS";
|
||||
|
||||
if (it > 2)
|
||||
recon_lock_duration = strtoll(params[2].c_str(), nullptr, 10);
|
||||
else
|
||||
recon_lock_duration = RECON_MIN_LOCK_DURATION;
|
||||
|
||||
if (it > 3)
|
||||
recon_lock_nb_match = strtoll(params[3].c_str(), nullptr, 10);
|
||||
else
|
||||
recon_lock_nb_match = RECON_DEF_NB_MATCH;
|
||||
|
||||
if (it > 4)
|
||||
squelch = strtoll(params[4].c_str(), nullptr, 10);
|
||||
else
|
||||
squelch = -14;
|
||||
|
||||
if (it > 5)
|
||||
recon_match_mode = strtoll(params[5].c_str(), nullptr, 10);
|
||||
else
|
||||
recon_match_mode = RECON_MATCH_CONTINUOUS;
|
||||
|
||||
if (it > 6)
|
||||
wait = strtoll(params[6].c_str(), nullptr, 10);
|
||||
else
|
||||
wait = RECON_DEF_WAIT_DURATION;
|
||||
|
||||
return true;
|
||||
return complete;
|
||||
}
|
||||
|
||||
bool ReconView::recon_save_config_to_sd() {
|
||||
@@ -486,6 +419,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
if (frequency_list.size() > 0) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(current_index);
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
// NB: This is using the freq keypad to select an index.
|
||||
f = f / OneMHz;
|
||||
if (f >= 1 && f <= frequency_list.size()) {
|
||||
index_stepper = f - 1 - current_index;
|
||||
@@ -496,17 +430,18 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_manual_start.on_change = [this]() {
|
||||
frequency_range.min = frequency_range.min + button_manual_start.get_encoder_delta() * freqman_entry_get_step_value(def_step);
|
||||
auto step_val = freqman_entry_get_step_value(def_step);
|
||||
frequency_range.min = frequency_range.min + button_manual_start.get_encoder_delta() * step_val;
|
||||
if (frequency_range.min < 0) {
|
||||
frequency_range.min = 0;
|
||||
}
|
||||
if (frequency_range.min > (MAX_UFREQ - freqman_entry_get_step_value(def_step))) {
|
||||
frequency_range.min = MAX_UFREQ - freqman_entry_get_step_value(def_step);
|
||||
if (frequency_range.min > (MAX_UFREQ - step_val)) {
|
||||
frequency_range.min = MAX_UFREQ - step_val;
|
||||
}
|
||||
if (frequency_range.min > (frequency_range.max - freqman_entry_get_step_value(def_step))) {
|
||||
frequency_range.max = frequency_range.min + freqman_entry_get_step_value(def_step);
|
||||
if (frequency_range.min > (frequency_range.max - step_val)) {
|
||||
frequency_range.max = frequency_range.min + step_val;
|
||||
if (frequency_range.max > MAX_UFREQ) {
|
||||
frequency_range.min = MAX_UFREQ - freqman_entry_get_step_value(def_step);
|
||||
frequency_range.min = MAX_UFREQ - step_val;
|
||||
frequency_range.max = MAX_UFREQ;
|
||||
}
|
||||
}
|
||||
@@ -516,18 +451,19 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_manual_end.on_change = [this]() {
|
||||
frequency_range.max = frequency_range.max + button_manual_end.get_encoder_delta() * freqman_entry_get_step_value(def_step);
|
||||
if (frequency_range.max < (freqman_entry_get_step_value(def_step) + 1)) {
|
||||
frequency_range.max = (freqman_entry_get_step_value(def_step) + 1);
|
||||
auto step_val = freqman_entry_get_step_value(def_step);
|
||||
frequency_range.max = frequency_range.max + button_manual_end.get_encoder_delta() * step_val;
|
||||
if (frequency_range.max < (step_val + 1)) {
|
||||
frequency_range.max = (step_val + 1);
|
||||
}
|
||||
if (frequency_range.max > MAX_UFREQ) {
|
||||
frequency_range.max = MAX_UFREQ;
|
||||
}
|
||||
if (frequency_range.max < (frequency_range.min + freqman_entry_get_step_value(def_step))) {
|
||||
frequency_range.min = frequency_range.max - freqman_entry_get_step_value(def_step);
|
||||
if (frequency_range.max < (freqman_entry_get_step_value(def_step) + 1)) {
|
||||
if (frequency_range.max < (frequency_range.min + step_val)) {
|
||||
frequency_range.min = frequency_range.max - step_val;
|
||||
if (frequency_range.max < (step_val + 1)) {
|
||||
frequency_range.min = 1;
|
||||
frequency_range.max = (freqman_entry_get_step_value(def_step) + 1);
|
||||
frequency_range.max = (step_val + 1);
|
||||
}
|
||||
}
|
||||
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
|
||||
@@ -607,87 +543,9 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_remove.on_select = [this](ButtonWithEncoder&) {
|
||||
// TODO: Use FreqmanDB
|
||||
if (frequency_list.size() > 0) {
|
||||
if (!manual_mode) {
|
||||
// scanner or recon (!scanner) mode
|
||||
// in both we delete index from live view, but only remove in output file in scanner_mode
|
||||
if (current_index >= (int32_t)frequency_list.size()) {
|
||||
current_index = frequency_list.size() - 1;
|
||||
}
|
||||
frequency_list.erase(frequency_list.begin() + current_index);
|
||||
if (current_index >= (int32_t)frequency_list.size()) {
|
||||
current_index = frequency_list.size() - 1;
|
||||
}
|
||||
if (frequency_list.size() > 0) {
|
||||
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
|
||||
update_description();
|
||||
}
|
||||
// also remove from output file if in scanner mode
|
||||
if (scanner_mode) {
|
||||
File freqman_file{};
|
||||
delete_file(freq_file_path);
|
||||
auto result = freqman_file.create(freq_file_path);
|
||||
if (!result.is_valid()) {
|
||||
for (size_t n = 0; n < frequency_list.size(); n++) {
|
||||
auto line = to_freqman_string(*frequency_list[n]);
|
||||
freqman_file.write_line(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (manual_mode) // only remove from output
|
||||
{
|
||||
File recon_file{};
|
||||
File tmp_recon_file{};
|
||||
std::string tmp_freq_file_path{freq_file_path + ".TMP"};
|
||||
handle_remove_current_item();
|
||||
|
||||
freqman_entry entry = current_entry();
|
||||
entry.frequency_a = freq;
|
||||
entry.frequency_b = 0;
|
||||
entry.modulation = last_entry.modulation;
|
||||
entry.bandwidth = last_entry.bandwidth;
|
||||
entry.type = freqman_type::Single;
|
||||
|
||||
auto frequency_to_add = to_freqman_string(entry);
|
||||
|
||||
delete_file(tmp_freq_file_path);
|
||||
auto result = tmp_recon_file.create(tmp_freq_file_path); // First recon if freq is already in txt
|
||||
if (!result.is_valid()) {
|
||||
bool found = false;
|
||||
result = recon_file.open(freq_file_path); // First recon if freq is already in txt
|
||||
if (!result.is_valid()) {
|
||||
char one_char[1]{}; // Read it char by char
|
||||
std::string line{}; // and put read line in here
|
||||
for (size_t pointer = 0; pointer < recon_file.size(); pointer++) {
|
||||
recon_file.seek(pointer);
|
||||
recon_file.read(one_char, 1);
|
||||
if ((int)one_char[0] > 31) { // ascii space upwards
|
||||
line += one_char[0]; // add it to the textline
|
||||
} else if (one_char[0] == '\n') { // new Line
|
||||
if (line.compare(0, frequency_to_add.size(), frequency_to_add) == 0) {
|
||||
found = true;
|
||||
} else {
|
||||
tmp_recon_file.write_line(frequency_to_add);
|
||||
}
|
||||
line.clear(); // ready for next textline
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
delete_file(freq_file_path);
|
||||
rename_file(tmp_freq_file_path, freq_file_path);
|
||||
} else {
|
||||
delete_file(tmp_freq_file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
receiver_model.set_target_frequency(current_entry().frequency_a); // retune
|
||||
}
|
||||
if (frequency_list.size() == 0) {
|
||||
text_cycle.set_text(" ");
|
||||
desc_cycle.set("no entries in list"); // Show new description
|
||||
delete_file(freq_file_path);
|
||||
}
|
||||
// TODO: refresh UI.
|
||||
timer = 0;
|
||||
freq_lock = 0;
|
||||
};
|
||||
@@ -766,9 +624,8 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_restart.on_select = [this](Button&) {
|
||||
frequency_file_load(true);
|
||||
if (frequency_list.size() > 0) {
|
||||
def_step = step_mode.selected_index(); // Use def_step from manual selector
|
||||
frequency_file_load(true);
|
||||
if (fwd) {
|
||||
button_dir.set_text("FW>");
|
||||
} else {
|
||||
@@ -921,8 +778,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
recon_redraw();
|
||||
}
|
||||
|
||||
void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
(void)(stop_all_before);
|
||||
void ReconView::frequency_file_load(bool) {
|
||||
if (field_mode.selected_index_value() != SPEC_MODULATION)
|
||||
audio::output::stop();
|
||||
|
||||
@@ -940,6 +796,7 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
desc_cycle.set_style(&Styles::blue);
|
||||
button_scanner_mode.set_text("RECON");
|
||||
}
|
||||
|
||||
freqman_load_options options{
|
||||
.load_freqs = load_freqs,
|
||||
.load_ranges = load_ranges,
|
||||
@@ -1430,4 +1287,35 @@ void ReconView::handle_coded_squelch(const uint32_t value) {
|
||||
text_ctcss.set(" ");
|
||||
}
|
||||
|
||||
void ReconView::handle_remove_current_item() {
|
||||
if (frequency_list.empty() || !current_is_valid())
|
||||
return;
|
||||
|
||||
auto entry = current_entry(); // Copy the current entry.
|
||||
|
||||
// In Scanner or Recon modes, remove from the in-memory list.
|
||||
if (mode() != recon_mode::Manual) {
|
||||
if (current_is_valid()) {
|
||||
frequency_list.erase(frequency_list.begin() + current_index);
|
||||
}
|
||||
|
||||
if (frequency_list.size() > 0) {
|
||||
current_index = clip<int32_t>(current_index, 0u, frequency_list.size() - 1);
|
||||
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
|
||||
update_description();
|
||||
} else {
|
||||
current_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// In Scanner or Manual mode, remove the entry from the output file.
|
||||
if (mode() != recon_mode::Recon) {
|
||||
FreqmanDB freq_db;
|
||||
if (!freq_db.open(freq_file_path))
|
||||
return;
|
||||
|
||||
freq_db.delete_entry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -46,6 +46,12 @@ namespace ui {
|
||||
|
||||
#define RECON_CFG_FILE "SETTINGS/recon.cfg"
|
||||
|
||||
enum class recon_mode : uint8_t {
|
||||
Recon,
|
||||
Scanner,
|
||||
Manual
|
||||
};
|
||||
|
||||
class ReconView : public View {
|
||||
public:
|
||||
ReconView(NavigationView& nav);
|
||||
@@ -82,9 +88,10 @@ class ReconView : public View {
|
||||
void recon_redraw();
|
||||
void handle_retune();
|
||||
void handle_coded_squelch(const uint32_t value);
|
||||
void handle_remove_current_item();
|
||||
bool recon_load_config_from_sd();
|
||||
bool recon_save_config_to_sd();
|
||||
bool recon_save_freq(const std::string& freq_file_path, size_t index, bool warn_if_exists);
|
||||
bool recon_save_freq(const std::filesystem::path& path, size_t index, bool warn_if_exists);
|
||||
// placeholder for possible void recon_start_recording();
|
||||
void recon_stop_recording();
|
||||
|
||||
@@ -92,8 +99,15 @@ class ReconView : public View {
|
||||
bool current_is_valid();
|
||||
freqman_entry& current_entry();
|
||||
|
||||
// TODO: consolidate mode bools and use recon_mode.
|
||||
recon_mode mode() const {
|
||||
if (manual_mode) return recon_mode::Manual;
|
||||
if (scanner_mode) return recon_mode::Scanner;
|
||||
return recon_mode::Recon;
|
||||
}
|
||||
|
||||
jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too...
|
||||
int32_t squelch{0};
|
||||
int32_t squelch{RECON_DEF_SQUELCH};
|
||||
int32_t db{0};
|
||||
int32_t timer{0};
|
||||
int32_t wait{RECON_DEF_WAIT_DURATION}; // in msec. if > 0 wait duration after a lock, if < 0 duration is set to 'wait' unless there is no more activity
|
||||
@@ -117,7 +131,7 @@ class ReconView : public View {
|
||||
bool user_pause{false};
|
||||
bool auto_record_locked{false};
|
||||
bool is_recording{false};
|
||||
uint32_t recon_lock_nb_match{3};
|
||||
uint32_t recon_lock_nb_match{RECON_DEF_NB_MATCH};
|
||||
uint32_t recon_lock_duration{RECON_MIN_LOCK_DURATION};
|
||||
uint32_t recon_match_mode{RECON_MATCH_CONTINUOUS};
|
||||
bool scanner_mode{false};
|
||||
|
||||
@@ -70,11 +70,9 @@ 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->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
std::string dir_filter = "FREQMAN/";
|
||||
std::string str_file_path = new_file_path.string();
|
||||
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
|
||||
if (new_file_path.native().find(freqman_dir.native()) == 0) {
|
||||
_output_file = new_file_path.stem().string();
|
||||
button_output_file.set_text(_output_file);
|
||||
text_input_file.set(_output_file);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
@@ -84,7 +82,7 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
button_output_file.on_select = [this, &nav](Button&) {
|
||||
text_prompt(nav, _output_file, 28,
|
||||
[this](std::string& buffer) {
|
||||
_output_file = buffer;
|
||||
_output_file = std::move(buffer);
|
||||
button_output_file.set_text(_output_file);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
// maximum lock duration
|
||||
#define RECON_MAX_LOCK_DURATION 9900
|
||||
|
||||
#define RECON_DEF_SQUELCH -14
|
||||
|
||||
// default number of match to have a lock
|
||||
#define RECON_DEF_NB_MATCH 3
|
||||
#define RECON_MIN_LOCK_DURATION 100 // have to be >= and a multiple of STATS_UPDATE_INTERVAL
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "ui_scanner.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
|
||||
@@ -480,10 +480,12 @@ class SetEncoderDialView : public View {
|
||||
|
||||
OptionsField field_encoder_dial_sensitivity{
|
||||
{20 * 8, 3 * 16},
|
||||
6,
|
||||
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
|
||||
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_MEDIUM},
|
||||
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
|
||||
7,
|
||||
{{"LOWEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOWEST},
|
||||
{"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}}};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
|
||||
@@ -20,13 +20,14 @@
|
||||
*/
|
||||
|
||||
#include "ui_spectrum_painter.hpp"
|
||||
|
||||
#include "bmp.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "file.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __FIELD_BINDER_H__
|
||||
#define __FIELD_BINDER_H__
|
||||
|
||||
/* There's a common pattern that appears in applications that use
|
||||
* field widgets. The field value is set, on_change and on_select
|
||||
* handlers are registered. on_select will read the value and
|
||||
* set the new value once changed. on_change will be called which
|
||||
* will write the new value to the backing variable. */
|
||||
|
||||
struct binding_traits {
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
constexpr auto get_traits() {
|
||||
return {};
|
||||
}
|
||||
|
||||
template <typename TField, typename TTraits = get_traits<TField>()>
|
||||
class FieldBinding {
|
||||
};
|
||||
|
||||
#endif /*__FIELD_BINDER_H__*/
|
||||
@@ -45,8 +45,11 @@ Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BY
|
||||
}
|
||||
}
|
||||
|
||||
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only) {
|
||||
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only, bool create) {
|
||||
BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE;
|
||||
if (create)
|
||||
mode |= FA_OPEN_ALWAYS;
|
||||
|
||||
return open_fatfs(filename, mode);
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ class File {
|
||||
File& operator=(const File&) = delete;
|
||||
|
||||
// TODO: Return Result<>.
|
||||
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true);
|
||||
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true, bool create = false);
|
||||
Optional<Error> append(const std::filesystem::path& filename);
|
||||
Optional<Error> create(const std::filesystem::path& filename);
|
||||
|
||||
|
||||
@@ -463,9 +463,9 @@ 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) {
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path, bool create = false) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path, /*read_only*/ false);
|
||||
auto error = fw->file_.open(path, /*read_only*/ false, create);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
|
||||
@@ -22,92 +22,16 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "file.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
using option_t = ui::OptionsField::option_t;
|
||||
using options_t = ui::OptionsField::options_t;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using namespace tonekey;
|
||||
using namespace ui;
|
||||
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
|
||||
extern options_t freqman_modulations;
|
||||
extern options_t freqman_bandwidths[4];
|
||||
extern options_t freqman_steps;
|
||||
extern options_t freqman_steps_short;
|
||||
|
||||
extern const option_t* find_by_index(const options_t& options, freqman_index_t index);
|
||||
|
||||
// TODO: remove in favor of FreqmanDB
|
||||
/* Freqman file handling. */
|
||||
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
|
||||
return parse_freqman_file(get_freqman_path(file_stem), db, options);
|
||||
}
|
||||
|
||||
bool delete_freqman_file(const std::string& file_stem) {
|
||||
delete_file(get_freqman_path(file_stem));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool save_freqman_file(const std::string& file_stem, freqman_db& db) {
|
||||
auto path = get_freqman_path(file_stem);
|
||||
delete_file(path);
|
||||
|
||||
File freqman_file;
|
||||
auto error = freqman_file.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
for (size_t n = 0; n < db.size(); n++)
|
||||
freqman_file.write_line(to_freqman_string(*db[n]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool create_freqman_file(const std::string& file_stem) {
|
||||
auto fs_error = make_new_file(get_freqman_path(file_stem));
|
||||
return fs_error.ok();
|
||||
}
|
||||
|
||||
/* Set options. */
|
||||
void freqman_set_modulation_option(OptionsField& option) {
|
||||
option.set_options(freqman_modulations);
|
||||
}
|
||||
|
||||
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
|
||||
if (is_valid(modulation))
|
||||
option.set_options(freqman_bandwidths[modulation]);
|
||||
}
|
||||
|
||||
void freqman_set_step_option(OptionsField& option) {
|
||||
option.set_options(freqman_steps);
|
||||
}
|
||||
|
||||
void freqman_set_step_option_short(OptionsField& option) {
|
||||
option.set_options(freqman_steps_short);
|
||||
}
|
||||
|
||||
/* Option value lookup. */
|
||||
// TODO: use Optional instead of magic values.
|
||||
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) {
|
||||
if (auto opt = find_by_index(freqman_modulations, modulation))
|
||||
return opt->second;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth) {
|
||||
if (modulation < freqman_modulations.size()) {
|
||||
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
|
||||
return opt->second;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t freqman_entry_get_step_value(freqman_index_t step) {
|
||||
if (auto opt = find_by_index(freqman_steps, step))
|
||||
return opt->second;
|
||||
|
||||
@@ -25,13 +25,7 @@
|
||||
#ifndef __FREQMAN_H__
|
||||
#define __FREQMAN_H__
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "file.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
// Defined for back-compat.
|
||||
#define FREQMAN_MAX_PER_FILE freqman_default_max_entries
|
||||
@@ -50,21 +44,7 @@ enum freqman_entry_modulation : uint8_t {
|
||||
SPEC_MODULATION
|
||||
};
|
||||
|
||||
// TODO: Replace with FreqmanDB.
|
||||
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
|
||||
bool delete_freqman_file(const std::string& file_stem);
|
||||
bool save_freqman_file(const std::string& file_stem, freqman_db& db);
|
||||
bool create_freqman_file(const std::string& file_stem);
|
||||
|
||||
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
|
||||
void freqman_set_modulation_option(ui::OptionsField& option);
|
||||
void freqman_set_step_option(ui::OptionsField& option);
|
||||
void freqman_set_step_option_short(ui::OptionsField& option);
|
||||
void freqman_set_tone_option(ui::OptionsField& option);
|
||||
|
||||
// TODO: Can these be removed after Recon is migrated to FreqmanDB?
|
||||
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation);
|
||||
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth);
|
||||
int32_t freqman_entry_get_step_value(freqman_index_t step);
|
||||
|
||||
#endif /*__FREQMAN_H__*/
|
||||
|
||||
@@ -40,10 +40,6 @@ namespace fs = std::filesystem;
|
||||
const std::filesystem::path freqman_dir{u"/FREQMAN"};
|
||||
const std::filesystem::path freqman_extension{u".TXT"};
|
||||
|
||||
const std::filesystem::path get_freqman_path(const std::string& stem) {
|
||||
return freqman_dir / stem + freqman_extension;
|
||||
}
|
||||
|
||||
// NB: Don't include UI headers to keep this code unit testable.
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
@@ -97,6 +93,7 @@ options_t freqman_bandwidths[4] = {
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: these should be indexes.
|
||||
options_t freqman_steps = {
|
||||
{"0.1kHz ", 100},
|
||||
{"1kHz ", 1000},
|
||||
@@ -116,6 +113,7 @@ options_t freqman_steps = {
|
||||
{"1MHz ", 1000000},
|
||||
};
|
||||
|
||||
// TODO: these should be indexes.
|
||||
options_t freqman_steps_short = {
|
||||
{"0.1kHz", 100},
|
||||
{"1kHz", 1000},
|
||||
@@ -162,6 +160,27 @@ const option_t* find_by_index(const options_t& options, freqman_index_t index) {
|
||||
*}
|
||||
*/
|
||||
|
||||
bool operator==(const freqman_entry& lhs, const freqman_entry& rhs) {
|
||||
auto equal = lhs.type == rhs.type &&
|
||||
lhs.frequency_a == rhs.frequency_a &&
|
||||
lhs.description == rhs.description &&
|
||||
lhs.modulation == rhs.modulation &&
|
||||
lhs.bandwidth == rhs.bandwidth;
|
||||
|
||||
if (!equal)
|
||||
return false;
|
||||
|
||||
if (lhs.type == freqman_type::Range) {
|
||||
equal = lhs.frequency_b == rhs.frequency_b &&
|
||||
lhs.step == rhs.step;
|
||||
} else if (lhs.type == freqman_type::HamRadio) {
|
||||
equal = lhs.frequency_b == rhs.frequency_b &&
|
||||
lhs.tone == rhs.tone;
|
||||
}
|
||||
|
||||
return equal;
|
||||
}
|
||||
|
||||
std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
|
||||
if (auto opt = find_by_index(freqman_modulations, modulation))
|
||||
return opt->first;
|
||||
@@ -188,6 +207,23 @@ std::string freqman_entry_get_step_string_short(freqman_index_t step) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::filesystem::path get_freqman_path(const std::string& stem) {
|
||||
return freqman_dir / stem + freqman_extension;
|
||||
}
|
||||
|
||||
bool create_freqman_file(const std::string& file_stem) {
|
||||
auto fs_error = make_new_file(get_freqman_path(file_stem));
|
||||
return fs_error.ok();
|
||||
}
|
||||
|
||||
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
|
||||
return parse_freqman_file(get_freqman_path(file_stem), db, options);
|
||||
}
|
||||
|
||||
void delete_freqman_file(const std::string& file_stem) {
|
||||
delete_file(get_freqman_path(file_stem));
|
||||
}
|
||||
|
||||
std::string pretty_string(const freqman_entry& entry, size_t max_length) {
|
||||
std::string str;
|
||||
|
||||
@@ -196,12 +232,12 @@ std::string pretty_string(const freqman_entry& entry, size_t max_length) {
|
||||
str = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
|
||||
break;
|
||||
case freqman_type::Range:
|
||||
str = to_string_dec_uint(entry.frequency_a / 1'000'000) + "M-" +
|
||||
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
|
||||
str = to_string_rounded_freq(entry.frequency_a, 1) + "M-" +
|
||||
to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
|
||||
break;
|
||||
case freqman_type::HamRadio:
|
||||
str = "R:" + to_string_dec_uint(entry.frequency_a / 1'000'000) + "M,T:" +
|
||||
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
|
||||
str = "R:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" +
|
||||
to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
|
||||
break;
|
||||
case freqman_type::Raw:
|
||||
str = entry.description;
|
||||
@@ -337,55 +373,23 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
|
||||
}
|
||||
}
|
||||
|
||||
// No valid frequency combination was set.
|
||||
if (entry.type == freqman_type::Unknown)
|
||||
return false;
|
||||
|
||||
// Frequency A must be set for all types
|
||||
if (entry.frequency_a == 0)
|
||||
return false;
|
||||
|
||||
// Frequency B must be set for type Range or Ham Radio
|
||||
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
|
||||
if (entry.frequency_b == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ranges should have frequencies A <= B.
|
||||
if (entry.type == freqman_type::Range) {
|
||||
if (entry.frequency_a > entry.frequency_b)
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Consider additional validation:
|
||||
// - Tone only on HamRadio.
|
||||
// - Fail on failed parse_int.
|
||||
// - Fail if bandwidth set before modulation.
|
||||
|
||||
return true;
|
||||
return is_valid(entry);
|
||||
}
|
||||
|
||||
// TODO: Use FreqmanDB iterator.
|
||||
bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) {
|
||||
File f;
|
||||
auto error = f.open(path);
|
||||
if (error)
|
||||
FreqmanDB freqman_db;
|
||||
freqman_db.set_read_raw(false); // Don't return malformed lines.
|
||||
if (!freqman_db.open(path))
|
||||
return false;
|
||||
|
||||
auto reader = FileLineReader(f);
|
||||
auto line_count = count_lines(reader);
|
||||
|
||||
// Attempt to avoid a re-alloc if possible.
|
||||
db.clear();
|
||||
db.reserve(line_count);
|
||||
|
||||
for (const auto& line : reader) {
|
||||
freqman_entry entry{};
|
||||
if (!parse_freqman_entry(line, entry))
|
||||
continue;
|
||||
db.reserve(freqman_db.entry_count());
|
||||
|
||||
for (auto entry : freqman_db) {
|
||||
// Filter by entry type.
|
||||
if ((entry.type == freqman_type::Single && !options.load_freqs) ||
|
||||
if (entry.type == freqman_type::Unknown ||
|
||||
(entry.type == freqman_type::Single && !options.load_freqs) ||
|
||||
(entry.type == freqman_type::Range && !options.load_ranges) ||
|
||||
(entry.type == freqman_type::HamRadio && !options.load_hamradios)) {
|
||||
continue;
|
||||
@@ -411,10 +415,40 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_valid(const freqman_entry& entry) {
|
||||
// No valid frequency combination was set.
|
||||
if (entry.type == freqman_type::Unknown)
|
||||
return false;
|
||||
|
||||
// Frequency A must be set for all types
|
||||
if (entry.frequency_a == 0)
|
||||
return false;
|
||||
|
||||
// Frequency B must be set for type Range or Ham Radio
|
||||
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
|
||||
if (entry.frequency_b == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ranges should have frequencies A <= B.
|
||||
if (entry.type == freqman_type::Range) {
|
||||
if (entry.frequency_a > entry.frequency_b)
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Consider additional validation:
|
||||
// - Tone only on HamRadio.
|
||||
// - Step only on Range
|
||||
// - Fail on failed parse_int.
|
||||
// - Fail if bandwidth set before modulation.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* FreqmanDB ***********************************/
|
||||
|
||||
bool FreqmanDB::open(const std::filesystem::path& path) {
|
||||
auto result = FileWrapper::open(path);
|
||||
bool FreqmanDB::open(const std::filesystem::path& path, bool create) {
|
||||
auto result = FileWrapper::open(path, create);
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
@@ -426,15 +460,15 @@ void FreqmanDB::close() {
|
||||
wrapper_.reset();
|
||||
}
|
||||
|
||||
freqman_entry FreqmanDB::operator[](FileWrapper::Line line) const {
|
||||
auto length = wrapper_->line_length(line);
|
||||
auto line_text = wrapper_->get_text(line, 0, length);
|
||||
freqman_entry FreqmanDB::operator[](Index index) const {
|
||||
auto length = wrapper_->line_length(index);
|
||||
auto line_text = wrapper_->get_text(index, 0, length);
|
||||
|
||||
if (line_text) {
|
||||
freqman_entry entry;
|
||||
if (parse_freqman_entry(*line_text, entry))
|
||||
return entry;
|
||||
else {
|
||||
else if (read_raw_) {
|
||||
entry.type = freqman_type::Raw;
|
||||
entry.description = trim(*line_text);
|
||||
return entry;
|
||||
@@ -444,15 +478,18 @@ freqman_entry FreqmanDB::operator[](FileWrapper::Line line) const {
|
||||
return {};
|
||||
}
|
||||
|
||||
void FreqmanDB::insert_entry(const freqman_entry& entry, FileWrapper::Line line) {
|
||||
// TODO: Can be more efficient.
|
||||
line = clip<uint32_t>(line, 0u, entry_count());
|
||||
wrapper_->insert_line(line);
|
||||
replace_entry(line, entry);
|
||||
void FreqmanDB::insert_entry(Index index, const freqman_entry& entry) {
|
||||
index = clip<uint32_t>(index, 0u, entry_count());
|
||||
wrapper_->insert_line(index);
|
||||
replace_entry(index, entry);
|
||||
}
|
||||
|
||||
void FreqmanDB::replace_entry(FileWrapper::Line line, const freqman_entry& entry) {
|
||||
auto range = wrapper_->line_range(line);
|
||||
void FreqmanDB::append_entry(const freqman_entry& entry) {
|
||||
insert_entry(entry_count(), entry);
|
||||
}
|
||||
|
||||
void FreqmanDB::replace_entry(Index index, const freqman_entry& entry) {
|
||||
auto range = wrapper_->line_range(index);
|
||||
if (!range)
|
||||
return;
|
||||
|
||||
@@ -461,8 +498,23 @@ void FreqmanDB::replace_entry(FileWrapper::Line line, const freqman_entry& entry
|
||||
wrapper_->replace_range(*range, to_freqman_string(entry));
|
||||
}
|
||||
|
||||
void FreqmanDB::delete_entry(FileWrapper::Line line) {
|
||||
wrapper_->delete_line(line);
|
||||
void FreqmanDB::delete_entry(Index index) {
|
||||
wrapper_->delete_line(index);
|
||||
}
|
||||
|
||||
bool FreqmanDB::delete_entry(const freqman_entry& entry) {
|
||||
auto it = find_entry(entry);
|
||||
if (it == end())
|
||||
return false;
|
||||
|
||||
delete_entry(it.index());
|
||||
return true;
|
||||
}
|
||||
|
||||
FreqmanDB::iterator FreqmanDB::find_entry(const freqman_entry& entry) {
|
||||
return find_entry([&entry](const auto& other) {
|
||||
return entry == other;
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t FreqmanDB::entry_count() const {
|
||||
|
||||
@@ -52,9 +52,6 @@ constexpr bool is_invalid(freqman_index_t index) {
|
||||
return index == freqman_invalid_index;
|
||||
}
|
||||
|
||||
/* Gets the full path for a given file stem (no extension). */
|
||||
const std::filesystem::path get_freqman_path(const std::string& stem);
|
||||
|
||||
enum class freqman_type : uint8_t {
|
||||
Single, // f=
|
||||
Range, // a=,b=
|
||||
@@ -151,6 +148,8 @@ struct freqman_entry {
|
||||
freqman_index_t tone{freqman_invalid_index};
|
||||
};
|
||||
|
||||
bool operator==(const freqman_entry& lhs, const freqman_entry& rhs);
|
||||
|
||||
// TODO: These shouldn't be exported.
|
||||
std::string freqman_entry_get_modulation_string(freqman_index_t modulation);
|
||||
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth);
|
||||
@@ -173,6 +172,12 @@ struct freqman_load_options {
|
||||
using freqman_entry_ptr = std::unique_ptr<freqman_entry>;
|
||||
using freqman_db = std::vector<freqman_entry_ptr>;
|
||||
|
||||
/* Gets the full path for a given file stem (no extension). */
|
||||
const std::filesystem::path get_freqman_path(const std::string& stem);
|
||||
bool create_freqman_file(const std::string& file_stem);
|
||||
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
|
||||
void delete_freqman_file(const std::string& file_stem);
|
||||
|
||||
/* Gets a pretty string representation for an entry. */
|
||||
std::string pretty_string(const freqman_entry& item, size_t max_length = 30);
|
||||
|
||||
@@ -182,54 +187,94 @@ std::string to_freqman_string(const freqman_entry& entry);
|
||||
bool parse_freqman_entry(std::string_view str, freqman_entry& entry);
|
||||
bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options);
|
||||
|
||||
/* The tricky part of using the file directly is that there can be comments
|
||||
* and empty lines in the file. This messes up the 'count' calculation.
|
||||
* Either have to live with 'count' being an upper bound have the callers
|
||||
* know to expect that entries may be empty. */
|
||||
// NB: This won't apply implicit mod/bandwidth.
|
||||
// TODO: Reuse for parse_freqman_file
|
||||
/* Returns true if the entry is well-formed. */
|
||||
bool is_valid(const freqman_entry& entry);
|
||||
|
||||
/* API wrapper over a Freqman file. Provides CRUD operations
|
||||
* for freqman_entry instances that are read/written directly
|
||||
* to the underlying file. */
|
||||
class FreqmanDB {
|
||||
public:
|
||||
using Index = FileWrapper::Line;
|
||||
|
||||
/* NB: This iterator is very basic: forward only, read-only. */
|
||||
class iterator {
|
||||
public:
|
||||
iterator(FreqmanDB& db, FileWrapper::Offset line)
|
||||
: db_{db}, line_{line} {}
|
||||
iterator(FreqmanDB& db, Index index)
|
||||
: db_{db}, index_{index} {}
|
||||
iterator& operator++() {
|
||||
line_++;
|
||||
index_++;
|
||||
|
||||
if (index_ >= db_.entry_count())
|
||||
index_ = end_index;
|
||||
|
||||
return *this;
|
||||
}
|
||||
freqman_entry operator*() const {
|
||||
return db_[line_];
|
||||
return db_[index_];
|
||||
}
|
||||
|
||||
bool operator==(const iterator& other) {
|
||||
return &db_ == &other.db_ && index_ == other.index_;
|
||||
}
|
||||
|
||||
bool operator!=(const iterator& other) {
|
||||
return &db_ != &other.db_ || line_ != other.line_;
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
Index index() const {
|
||||
return index_;
|
||||
}
|
||||
|
||||
/* Value indicating the 'end' iterator. */
|
||||
static constexpr Index end_index = (Index)-1;
|
||||
|
||||
private:
|
||||
FreqmanDB& db_;
|
||||
FileWrapper::Line line_;
|
||||
Index index_;
|
||||
};
|
||||
|
||||
bool open(const std::filesystem::path& path);
|
||||
bool open(const std::filesystem::path& path, bool create = false);
|
||||
void close();
|
||||
freqman_entry operator[](FileWrapper::Line line) const;
|
||||
void insert_entry(const freqman_entry& entry, FileWrapper::Line line);
|
||||
void replace_entry(FileWrapper::Line line, const freqman_entry& entry);
|
||||
void delete_entry(FileWrapper::Line line);
|
||||
|
||||
freqman_entry operator[](Index index) const;
|
||||
void insert_entry(Index index, const freqman_entry& entry);
|
||||
void append_entry(const freqman_entry& entry);
|
||||
void replace_entry(Index index, const freqman_entry& entry);
|
||||
void delete_entry(Index index);
|
||||
bool delete_entry(const freqman_entry& entry);
|
||||
|
||||
template <typename Fn>
|
||||
iterator find_entry(const Fn& predicate) {
|
||||
// TODO: use std::find, but need to make the iterator compliant.
|
||||
auto it = begin();
|
||||
const auto it_end = end();
|
||||
|
||||
while (it != it_end) {
|
||||
if (predicate(*it))
|
||||
return it;
|
||||
++it;
|
||||
}
|
||||
|
||||
return it_end;
|
||||
}
|
||||
iterator find_entry(const freqman_entry& entry);
|
||||
|
||||
uint32_t entry_count() const;
|
||||
bool empty() const;
|
||||
|
||||
/* When true, Raw entries are returned instead of Unknown. */
|
||||
void set_read_raw(bool v) { read_raw_ = v; }
|
||||
iterator begin() {
|
||||
return {*this, 0};
|
||||
}
|
||||
iterator end() {
|
||||
return {*this, entry_count()};
|
||||
return {*this, iterator::end_index};
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<FileWrapper> wrapper_{};
|
||||
bool read_raw_{true};
|
||||
};
|
||||
|
||||
#endif /* __FREQMAN_DB_H__ */
|
||||
@@ -32,66 +32,24 @@
|
||||
// 12 degrees of rotation.
|
||||
//
|
||||
// For each encoder "pulse" there are 4 state transitions, and we can choose
|
||||
// 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
|
||||
},
|
||||
// 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
|
||||
};
|
||||
|
||||
int_fast8_t Encoder::update(
|
||||
@@ -102,6 +60,13 @@ int_fast8_t Encoder::update(
|
||||
state <<= 1;
|
||||
state |= phase_1;
|
||||
|
||||
// dial sensitivity setting is stored in pmem
|
||||
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class Encoder {
|
||||
|
||||
private:
|
||||
uint_fast8_t state{0};
|
||||
int_fast8_t transition_count{0};
|
||||
};
|
||||
|
||||
#endif /*__ENCODER_H__*/
|
||||
|
||||
@@ -162,13 +162,47 @@ std::string to_string_decimal(float decimal, int8_t precision) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// right-justified frequency in Hz, always 10 characters
|
||||
std::string to_string_freq(const uint64_t f) {
|
||||
auto final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
|
||||
std::string final_str{""};
|
||||
|
||||
if (f < 1000000)
|
||||
final_str = to_string_dec_int(f, 10, ' ');
|
||||
else
|
||||
final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
|
||||
|
||||
return final_str;
|
||||
}
|
||||
|
||||
// right-justified frequency in MHz, rounded to 4 decimal places, always 9 characters
|
||||
std::string to_string_short_freq(const uint64_t f) {
|
||||
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int((f / 100) % 10000, 4, '0');
|
||||
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int(((f + 50) / 100) % 10000, 4, '0');
|
||||
return final_str;
|
||||
}
|
||||
|
||||
// non-justified non-padded frequency in MHz, rounded to specified number of decimal places
|
||||
std::string to_string_rounded_freq(const uint64_t f, int8_t precision) {
|
||||
std::string final_str{""};
|
||||
static constexpr uint32_t pow10[7] = {
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
};
|
||||
|
||||
if (precision < 1) {
|
||||
final_str = to_string_dec_uint64(f / 1000000);
|
||||
} else {
|
||||
if (precision > 6)
|
||||
precision = 6;
|
||||
|
||||
uint32_t divisor = pow10[6 - precision];
|
||||
|
||||
final_str = to_string_dec_uint64(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
|
||||
}
|
||||
return final_str;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ std::string to_string_decimal(float decimal, int8_t precision);
|
||||
std::string to_string_hex(const uint64_t n, const int32_t l = 0);
|
||||
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
|
||||
|
||||
// NB: These pad-left and don't work correctly for values less than 1M.
|
||||
std::string to_string_freq(const uint64_t f);
|
||||
std::string to_string_short_freq(const uint64_t f);
|
||||
std::string to_string_rounded_freq(const uint64_t f, int8_t precision);
|
||||
std::string to_string_time_ms(const uint32_t ms);
|
||||
|
||||
std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format = YMDHMS);
|
||||
|
||||
@@ -48,7 +48,7 @@ class TextEntryView : public View {
|
||||
void char_add(const char c);
|
||||
void char_delete();
|
||||
|
||||
TextField text_input;
|
||||
TextEdit text_input;
|
||||
Button button_ok{
|
||||
{22 * 8, 33 * 8, 7 * 8, 32},
|
||||
"OK"};
|
||||
|
||||
+57
-12
@@ -22,26 +22,71 @@
|
||||
#ifndef __CONVERT_H__
|
||||
#define __CONVERT_H__
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
/* Zero-allocation conversion helper. */
|
||||
/* Notes:
|
||||
* - T must be an integer type.
|
||||
* - For base 16, input _must not_ contain a '0x' or '0X' prefix.
|
||||
* - For base 8 a leading 0 on the literal is allowed.
|
||||
* - Leading whitespce will cause the parse to fail.
|
||||
*/
|
||||
// TODO: from_chars seems to cause code bloat.
|
||||
// Look into using strtol and friends instead.
|
||||
constexpr size_t max_parse_int_length = 64;
|
||||
|
||||
/* This undefined type is used to see the size of the
|
||||
* unhandled type and to create a compilation error. */
|
||||
template <size_t N>
|
||||
struct UnhandledSize;
|
||||
|
||||
/* Returns true when errno is ERANGE. */
|
||||
inline bool range_error() {
|
||||
return errno == ERANGE;
|
||||
}
|
||||
|
||||
/* Assigns 'value' to 'out_val' and returns true if 'value' is
|
||||
* in bounds for type TOut. */
|
||||
template <typename TVal, typename TOut>
|
||||
bool checked_assign(TVal value, TOut& out_val) {
|
||||
// No chance for overflow, just return.
|
||||
if constexpr (sizeof(TVal) <= sizeof(TOut)) {
|
||||
out_val = value;
|
||||
return true;
|
||||
}
|
||||
out_val = static_cast<TOut>(value);
|
||||
return value >= static_cast<TVal>(std::numeric_limits<TOut>::min()) &&
|
||||
value <= static_cast<TVal>(std::numeric_limits<TOut>::max());
|
||||
}
|
||||
|
||||
/* Zero-allocation conversion helper. 'str' must be smaller than 'max_parse_int_length'. */
|
||||
template <typename T>
|
||||
std::enable_if_t<std::is_integral_v<T>, bool> parse_int(std::string_view str, T& out_val, int base = 10) {
|
||||
auto result = std::from_chars(str.data(), str.data() + str.length(), out_val, base);
|
||||
return static_cast<int>(result.ec) == 0;
|
||||
// Always initialize the output.
|
||||
out_val = {};
|
||||
|
||||
if (str.size() > max_parse_int_length)
|
||||
return false;
|
||||
|
||||
// Copy onto the stack and null terminate.
|
||||
char zstr[max_parse_int_length + 1];
|
||||
std::memcpy(zstr, str.data(), str.size());
|
||||
zstr[str.size()] = '\0';
|
||||
|
||||
// A little C++ type magic to select the correct conversion function.
|
||||
if constexpr (sizeof(T) == sizeof(long long)) {
|
||||
if constexpr (std::is_unsigned_v<T>)
|
||||
return checked_assign(strtoull(zstr, nullptr, base), out_val) && !range_error();
|
||||
else
|
||||
return checked_assign(strtoll(zstr, nullptr, base), out_val) && !range_error();
|
||||
|
||||
} else if constexpr (sizeof(T) <= sizeof(long)) {
|
||||
if constexpr (std::is_unsigned_v<T>)
|
||||
return checked_assign(strtoul(zstr, nullptr, base), out_val) && !range_error();
|
||||
else
|
||||
return checked_assign(strtol(zstr, nullptr, base), out_val) && !range_error();
|
||||
} else {
|
||||
UnhandledSize<sizeof(T) * 8> unhandled_case;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif /*__CONVERT_H__*/
|
||||
@@ -269,7 +269,7 @@ struct data_t {
|
||||
frequency_rx_correction(0),
|
||||
frequency_tx_correction(0),
|
||||
|
||||
encoder_dial_sensitivity(0),
|
||||
encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL),
|
||||
UNUSED_8(0),
|
||||
headphone_volume_cb(-600),
|
||||
misc_config(),
|
||||
@@ -363,6 +363,7 @@ void defaults() {
|
||||
|
||||
set_config_backlight_timer(backlight_config_t{});
|
||||
set_config_splash(true);
|
||||
set_encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL);
|
||||
|
||||
// Default values for recon app.
|
||||
set_recon_autosave_freqs(false);
|
||||
|
||||
@@ -107,9 +107,11 @@ struct backlight_config_t {
|
||||
};
|
||||
|
||||
enum encoder_dial_sensitivity {
|
||||
DIAL_SENSITIVITY_MEDIUM = 0,
|
||||
DIAL_SENSITIVITY_LOW = 1,
|
||||
DIAL_SENSITIVITY_HIGH = 2,
|
||||
DIAL_SENSITIVITY_HIGHEST = 0,
|
||||
DIAL_SENSITIVITY_HIGH = 1,
|
||||
DIAL_SENSITIVITY_NORMAL = 2,
|
||||
DIAL_SENSITIVITY_LOW = 3,
|
||||
DIAL_SENSITIVITY_LOWEST = 4,
|
||||
NUM_DIAL_SENSITIVITY
|
||||
};
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ Text::Text(
|
||||
Rect parent_rect,
|
||||
std::string text)
|
||||
: Widget{parent_rect},
|
||||
text{text} {
|
||||
text{std::move(text)} {
|
||||
}
|
||||
|
||||
Text::Text(
|
||||
@@ -357,14 +357,14 @@ Text::Text(
|
||||
: Text{parent_rect, {}} {
|
||||
}
|
||||
|
||||
void Text::set(const std::string value) {
|
||||
text = value;
|
||||
void Text::set(std::string_view value) {
|
||||
text = std::string{value};
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void Text::paint(Painter& painter) {
|
||||
const auto rect = screen_rect();
|
||||
const auto s = style();
|
||||
auto s = has_focus() ? style().invert() : style();
|
||||
|
||||
painter.fill_rectangle(rect, s.background);
|
||||
|
||||
@@ -1608,9 +1608,9 @@ bool OptionsField::on_touch(const TouchEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* TextField ***********************************************************/
|
||||
/* TextEdit ***********************************************************/
|
||||
|
||||
TextField::TextField(
|
||||
TextEdit::TextEdit(
|
||||
std::string& str,
|
||||
size_t max_length,
|
||||
Point position,
|
||||
@@ -1624,24 +1624,24 @@ TextField::TextField(
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
const std::string& TextField::value() const {
|
||||
const std::string& TextEdit::value() const {
|
||||
return text_;
|
||||
}
|
||||
|
||||
void TextField::set_cursor(uint32_t pos) {
|
||||
void TextEdit::set_cursor(uint32_t pos) {
|
||||
cursor_pos_ = std::min<size_t>(pos, text_.length());
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextField::set_insert_mode() {
|
||||
void TextEdit::set_insert_mode() {
|
||||
insert_mode_ = true;
|
||||
}
|
||||
|
||||
void TextField::set_overwrite_mode() {
|
||||
void TextEdit::set_overwrite_mode() {
|
||||
insert_mode_ = false;
|
||||
}
|
||||
|
||||
void TextField::char_add(char c) {
|
||||
void TextEdit::char_add(char c) {
|
||||
// Don't add if inserting and at max_length and
|
||||
// don't overwrite if past the end of the text.
|
||||
if ((text_.length() >= max_length_ && insert_mode_) ||
|
||||
@@ -1657,7 +1657,7 @@ void TextField::char_add(char c) {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextField::char_delete() {
|
||||
void TextEdit::char_delete() {
|
||||
if (cursor_pos_ == 0)
|
||||
return;
|
||||
|
||||
@@ -1666,7 +1666,7 @@ void TextField::char_delete() {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextField::paint(Painter& painter) {
|
||||
void TextEdit::paint(Painter& painter) {
|
||||
constexpr int char_width = 8;
|
||||
|
||||
auto rect = screen_rect();
|
||||
@@ -1702,7 +1702,7 @@ void TextField::paint(Painter& painter) {
|
||||
painter.draw_rectangle(cursor_box, cursor_style.background);
|
||||
}
|
||||
|
||||
bool TextField::on_key(const KeyEvent key) {
|
||||
bool TextEdit::on_key(const KeyEvent key) {
|
||||
if (key == KeyEvent::Left && cursor_pos_ > 0)
|
||||
cursor_pos_--;
|
||||
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
|
||||
@@ -1716,7 +1716,7 @@ bool TextField::on_key(const KeyEvent key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextField::on_encoder(const EncoderEvent delta) {
|
||||
bool TextEdit::on_encoder(const EncoderEvent delta) {
|
||||
int32_t new_pos = cursor_pos_ + delta;
|
||||
|
||||
// Let the encoder wrap around the ends of the text.
|
||||
@@ -1729,7 +1729,7 @@ bool TextField::on_encoder(const EncoderEvent delta) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextField::on_touch(const TouchEvent event) {
|
||||
bool TextEdit::on_touch(const TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start)
|
||||
focus();
|
||||
|
||||
@@ -1737,6 +1737,32 @@ bool TextField::on_touch(const TouchEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* TextField *************************************************************/
|
||||
|
||||
TextField::TextField(Rect parent_rect, std::string text)
|
||||
: Text(parent_rect, std::move(text)) {
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
const std::string& TextField::get_text() const {
|
||||
return text;
|
||||
}
|
||||
|
||||
void TextField::set_text(std::string_view value) {
|
||||
set(value);
|
||||
if (on_change)
|
||||
on_change(*this);
|
||||
}
|
||||
|
||||
bool TextField::on_key(KeyEvent key) {
|
||||
if (key == KeyEvent::Select && on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* NumberField ***********************************************************/
|
||||
|
||||
NumberField::NumberField(
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
#include "portapack.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -202,11 +203,11 @@ class Text : public Widget {
|
||||
Text(Rect parent_rect, std::string text);
|
||||
Text(Rect parent_rect);
|
||||
|
||||
void set(const std::string value);
|
||||
void set(std::string_view value);
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
protected:
|
||||
std::string text;
|
||||
};
|
||||
|
||||
@@ -619,6 +620,7 @@ class OptionsField : public Widget {
|
||||
|
||||
OptionsField(Point parent_pos, size_t length, options_t options);
|
||||
|
||||
options_t& options() { return options_; }
|
||||
const options_t& options() const { return options_; }
|
||||
void set_options(options_t new_options);
|
||||
|
||||
@@ -642,14 +644,14 @@ class OptionsField : public Widget {
|
||||
size_t selected_index_{0};
|
||||
};
|
||||
|
||||
// A TextField is bound to a string reference and allows the string
|
||||
// A TextEdit is bound to a string reference and allows the string
|
||||
// to be manipulated. The field itself does not provide the UI for
|
||||
// setting the value. It provides the UI of rendering the text,
|
||||
// a cursor, and an API to edit the string content.
|
||||
class TextField : public Widget {
|
||||
class TextEdit : public Widget {
|
||||
public:
|
||||
TextField(std::string& str, Point position, uint32_t length = 30)
|
||||
: TextField{str, 64, position, length} {}
|
||||
TextEdit(std::string& str, Point position, uint32_t length = 30)
|
||||
: TextEdit{str, 64, position, length} {}
|
||||
|
||||
// Str: the string containing the content to edit.
|
||||
// Max_length: max length the string is allowed to use.
|
||||
@@ -658,12 +660,12 @@ class TextField : public Widget {
|
||||
// - Characters are 8 pixels wide.
|
||||
// - The screen can show 30 characters max.
|
||||
// - The control is 16 pixels tall.
|
||||
TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30);
|
||||
TextEdit(std::string& str, size_t max_length, Point position, uint32_t length = 30);
|
||||
|
||||
TextField(const TextField&) = delete;
|
||||
TextField(TextField&&) = delete;
|
||||
TextField& operator=(const TextField&) = delete;
|
||||
TextField& operator=(TextField&&) = delete;
|
||||
TextEdit(const TextEdit&) = delete;
|
||||
TextEdit(TextEdit&&) = delete;
|
||||
TextEdit& operator=(const TextEdit&) = delete;
|
||||
TextEdit& operator=(TextEdit&&) = delete;
|
||||
|
||||
const std::string& value() const;
|
||||
|
||||
@@ -688,6 +690,22 @@ class TextField : public Widget {
|
||||
bool insert_mode_;
|
||||
};
|
||||
|
||||
class TextField : public Text {
|
||||
public:
|
||||
std::function<void(TextField&)> on_select{};
|
||||
std::function<void(TextField&)> on_change{};
|
||||
|
||||
TextField(Rect parent_rect, std::string text);
|
||||
|
||||
const std::string& get_text() const;
|
||||
void set_text(std::string_view value);
|
||||
|
||||
bool on_key(KeyEvent key) override;
|
||||
|
||||
private:
|
||||
using Text::set;
|
||||
};
|
||||
|
||||
class NumberField : public Widget {
|
||||
public:
|
||||
std::function<void(NumberField&)> on_select{};
|
||||
|
||||
@@ -71,6 +71,7 @@ target_include_directories(application_test PRIVATE
|
||||
)
|
||||
|
||||
target_compile_options(application_test PRIVATE
|
||||
-std=c++17
|
||||
-DLPC43XX
|
||||
-DLPC43XX_M0
|
||||
-D__NEWLIB__
|
||||
|
||||
@@ -46,10 +46,11 @@ TEST_CASE("It should convert string_views.") {
|
||||
CHECK_EQ(val, 12345);
|
||||
}
|
||||
|
||||
TEST_CASE("It should return false for invalid input") {
|
||||
// 'from_chars' supported this, but 'strtol' just returns 0.
|
||||
/*TEST_CASE("It should return false for invalid input") {
|
||||
int val = 0;
|
||||
REQUIRE_FALSE(parse_int("QWERTY", val));
|
||||
}
|
||||
}*/
|
||||
|
||||
TEST_CASE("It should return false for overflow input") {
|
||||
uint8_t val = 0;
|
||||
@@ -58,7 +59,7 @@ TEST_CASE("It should return false for overflow input") {
|
||||
|
||||
TEST_CASE("It should convert base 16.") {
|
||||
int val = 0;
|
||||
REQUIRE(parse_int("30", val, 16)); // NB: No '0x'
|
||||
REQUIRE(parse_int("0x30", val, 16));
|
||||
CHECK_EQ(val, 0x30);
|
||||
}
|
||||
|
||||
@@ -74,4 +75,28 @@ TEST_CASE("It should convert as much of the string as it can.") {
|
||||
CHECK_EQ(val, 12345);
|
||||
}
|
||||
|
||||
TEST_CASE("It should convert 64-bit.") {
|
||||
int64_t val = 0;
|
||||
REQUIRE(parse_int("123456789", val));
|
||||
CHECK_EQ(val, 123456789);
|
||||
}
|
||||
|
||||
TEST_CASE("It should convert 32-bit.") {
|
||||
int32_t val = 0;
|
||||
REQUIRE(parse_int("123456", val));
|
||||
CHECK_EQ(val, 123456);
|
||||
}
|
||||
|
||||
TEST_CASE("It should convert 16-bit.") {
|
||||
int16_t val = 0;
|
||||
REQUIRE(parse_int("12345", val));
|
||||
CHECK_EQ(val, 12345);
|
||||
}
|
||||
|
||||
TEST_CASE("It should convert 8-bit.") {
|
||||
int8_t val = 0;
|
||||
REQUIRE(parse_int("123", val));
|
||||
CHECK_EQ(val, 123);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -32,13 +32,41 @@ TEST_CASE("to_string_dec_uint64 returns correct value.") {
|
||||
CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891");
|
||||
}
|
||||
|
||||
/*TEST_CASE("to_string_freq returns correct value.") {
|
||||
CHECK_EQ(to_string_freq(0), "0");
|
||||
CHECK_EQ(to_string_freq(1), "1");
|
||||
CHECK_EQ(to_string_freq(1'000'000), "1000000");
|
||||
TEST_CASE("to_string_freq returns correct value.") {
|
||||
CHECK_EQ(to_string_freq(0), " 0");
|
||||
CHECK_EQ(to_string_freq(1), " 1");
|
||||
CHECK_EQ(to_string_freq(1'000'000), " 1000000");
|
||||
CHECK_EQ(to_string_freq(1'234'567'890), "1234567890");
|
||||
CHECK_EQ(to_string_freq(1'234'567'891), "1234567891");
|
||||
}*/
|
||||
}
|
||||
|
||||
TEST_CASE("to_string_short_freq returns correct value.") {
|
||||
CHECK_EQ(to_string_short_freq(0), " 0.0000");
|
||||
CHECK_EQ(to_string_short_freq(49), " 0.0000");
|
||||
CHECK_EQ(to_string_short_freq(50), " 0.0001");
|
||||
CHECK_EQ(to_string_short_freq(1'000'049), " 1.0000");
|
||||
CHECK_EQ(to_string_short_freq(1'000'050), " 1.0001");
|
||||
CHECK_EQ(to_string_short_freq(1'234'567'850), "1234.5679");
|
||||
CHECK_EQ(to_string_short_freq(1'234'567'849), "1234.5678");
|
||||
}
|
||||
|
||||
TEST_CASE("to_string_rounded_freq returns correct value.") {
|
||||
CHECK_EQ(to_string_rounded_freq(0, 0), "0");
|
||||
CHECK_EQ(to_string_rounded_freq(0, 2), "0.00");
|
||||
CHECK_EQ(to_string_rounded_freq(49, 4), "0.0000");
|
||||
CHECK_EQ(to_string_rounded_freq(50, 4), "0.0001");
|
||||
CHECK_EQ(to_string_rounded_freq(23'456, 4), "0.0235");
|
||||
CHECK_EQ(to_string_rounded_freq(1'000'000, 4), "1.0000");
|
||||
CHECK_EQ(to_string_rounded_freq(1'000'567'849, 4), "1000.5678");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 0), "1234");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 1), "1234.6");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 2), "1234.57");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 3), "1234.568");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 4), "1234.5679");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 5), "1234.56789");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 6), "1234.567891");
|
||||
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 9), "1234.567891");
|
||||
}
|
||||
|
||||
TEST_CASE("trim removes whitespace.") {
|
||||
CHECK(trim(" foo\n") == "foo");
|
||||
|
||||
Reference in New Issue
Block a user