First pass at custom app-settings support (#1381)

* First draft of custom app settings support.

* WIP new settings

* Working per-app custom settings

* Revert design to use "bound settings"
This commit is contained in:
Kyle Reed 2023-08-18 12:35:41 -07:00 committed by GitHub
parent a4636d7872
commit 63f99742fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 269 additions and 43 deletions

View File

@ -23,7 +23,9 @@
#include "app_settings.hpp" #include "app_settings.hpp"
#include "convert.hpp"
#include "file.hpp" #include "file.hpp"
#include "file_reader.hpp"
#include "portapack.hpp" #include "portapack.hpp"
#include "portapack_persistent_memory.hpp" #include "portapack_persistent_memory.hpp"
#include "utility.hpp" #include "utility.hpp"
@ -36,6 +38,126 @@ namespace fs = std::filesystem;
using namespace portapack; using namespace portapack;
using namespace std::literals; using namespace std::literals;
namespace {
fs::path get_settings_path(const std::string& app_name) {
return fs::path{u"/SETTINGS"} / app_name + u".ini";
}
} // namespace
void BoundSetting::parse(std::string_view value) {
switch (type_) {
case SettingType::I64:
parse_int(value, as<int64_t>());
break;
case SettingType::I32:
parse_int(value, as<int32_t>());
break;
case SettingType::U32:
parse_int(value, as<uint32_t>());
break;
case SettingType::U8:
parse_int(value, as<uint8_t>());
break;
case SettingType::String:
as<std::string>() = std::string{value};
break;
case SettingType::Bool: {
int parsed = 0;
parse_int(value, parsed);
as<bool>() = (parsed != 0);
break;
}
};
}
void BoundSetting::write(File& file) const {
// NB: Write directly without allocations. This happens on every
// app exit when enabled so should be fast to keep the UX responsive.
StringFormatBuffer buffer;
size_t length = 0;
file.write(name_.data(), name_.length());
file.write("=", 1);
switch (type_) {
case SettingType::I64:
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
break;
case SettingType::I32:
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
break;
case SettingType::U32:
file.write(to_string_dec_uint(as<uint32_t>(), buffer, length), length);
break;
case SettingType::U8:
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
break;
case SettingType::String: {
const auto& str = as<std::string>();
file.write(str.data(), str.length());
break;
}
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
break;
}
file.write("\r\n", 2);
}
SettingsStore::SettingsStore(std::string_view store_name, SettingBindings bindings)
: store_name_{store_name}, bindings_{bindings} {
load_settings(store_name_, bindings_);
}
SettingsStore::~SettingsStore() {
save_settings(store_name_, bindings_);
}
bool load_settings(std::string_view store_name, SettingBindings& bindings) {
File f;
auto path = get_settings_path(std::string{store_name});
auto error = f.open(path);
if (error)
return false;
auto reader = FileLineReader(f);
for (const auto& line : reader) {
auto cols = split_string(line, '=');
if (cols.size() != 2)
continue;
// Find a binding with the name.
auto it = std::find_if(
bindings.begin(), bindings.end(),
[name = cols[0]](auto& bound_setting) {
return name == bound_setting.name();
});
// If found, parse the value.
if (it != bindings.end())
it->parse(cols[1]);
}
return true;
}
bool save_settings(std::string_view store_name, const SettingBindings& bindings) {
File f;
auto path = get_settings_path(std::string{store_name});
auto error = f.create(path);
if (error)
return false;
for (const auto& bound_setting : bindings)
bound_setting.write(f);
return true;
}
namespace app_settings { namespace app_settings {
template <typename T> template <typename T>
@ -64,10 +186,6 @@ static void write_setting(File& file, std::string_view setting_name, const T& va
file.write("\r\n", 2); file.write("\r\n", 2);
} }
static fs::path get_settings_path(const std::string& app_name) {
return fs::path{u"/SETTINGS"} / app_name + u".ini";
}
namespace setting { namespace setting {
constexpr std::string_view baseband_bandwidth = "baseband_bandwidth="sv; constexpr std::string_view baseband_bandwidth = "baseband_bandwidth="sv;
constexpr std::string_view sampling_rate = "sampling_rate="sv; constexpr std::string_view sampling_rate = "sampling_rate="sv;
@ -88,13 +206,6 @@ constexpr std::string_view squelch = "squelch="sv;
constexpr std::string_view volume = "volume="sv; constexpr std::string_view volume = "volume="sv;
} // namespace setting } // namespace setting
// TODO: Only load/save values that are declared used.
// This will prevent switching apps from changing setting unnecessarily.
// TODO: Track which values are actually read.
// TODO: Maybe just use a dictionary which would allow for custom settings.
// TODO: Create a control value binding which will allow controls to
// be declaratively bound to a setting and persistence will be magic.
ResultCode load_settings(const std::string& app_name, AppSettings& settings) { ResultCode load_settings(const std::string& app_name, AppSettings& settings) {
if (!portapack::persistent_memory::load_app_settings()) if (!portapack::persistent_memory::load_app_settings())
return ResultCode::SettingsDisabled; return ResultCode::SettingsDisabled;

View File

@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek * Copyright (C) 2016 Furrtek
* Copyright (C) 2022 Arjan Onwezen * Copyright (C) 2022 Arjan Onwezen
* Copyright (C) 2023 Kyle Reed
* *
* This file is part of PortaPack. * This file is part of PortaPack.
* *
@ -27,12 +28,77 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <string_view>
#include <utility> #include <utility>
#include <variant>
#include "file.hpp" #include "file.hpp"
#include "max283x.hpp" #include "max283x.hpp"
#include "string_format.hpp" #include "string_format.hpp"
/* Represents a named setting bound to a variable instance. */
/* Using void* instead of std::variant, because variant is a pain to dispatch over. */
class BoundSetting {
/* The type of bound setting. */
enum class SettingType : uint8_t {
I64,
I32,
U32,
U8,
String,
Bool,
};
public:
BoundSetting(std::string_view name, int64_t* target)
: name_{name}, target_{target}, type_{SettingType::I64} {}
BoundSetting(std::string_view name, int32_t* target)
: name_{name}, target_{target}, type_{SettingType::I32} {}
BoundSetting(std::string_view name, uint32_t* target)
: name_{name}, target_{target}, type_{SettingType::U32} {}
BoundSetting(std::string_view name, uint8_t* target)
: name_{name}, target_{target}, type_{SettingType::U8} {}
BoundSetting(std::string_view name, std::string* target)
: name_{name}, target_{target}, type_{SettingType::String} {}
BoundSetting(std::string_view name, bool* target)
: name_{name}, target_{target}, type_{SettingType::Bool} {}
std::string_view name() const { return name_; }
void parse(std::string_view value);
void write(File& file) const;
private:
template <typename T>
constexpr auto& as() const {
return *reinterpret_cast<T*>(target_);
}
std::string_view name_;
void* target_;
SettingType type_;
};
using SettingBindings = std::vector<BoundSetting>;
/* RAII wrapper for Settings that loads/saves to the SD card. */
class SettingsStore {
public:
SettingsStore(std::string_view store_name, SettingBindings bindings);
~SettingsStore();
private:
std::string_view store_name_;
SettingBindings bindings_;
};
bool load_settings(std::string_view store_name, SettingBindings& bindings);
bool save_settings(std::string_view store_name, const SettingBindings& bindings);
namespace app_settings { namespace app_settings {
enum class ResultCode : uint8_t { enum class ResultCode : uint8_t {

View File

@ -71,6 +71,8 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
if (!settings_.loaded()) if (!settings_.loaded())
field_frequency.set_value(initial_target_frequency); field_frequency.set_value(initial_target_frequency);
check_log.set_value(enable_logging);
receiver_model.enable(); receiver_model.enable();
// TODO: app setting instead? // TODO: app setting instead?
@ -88,7 +90,6 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
logger->append(LOG_ROOT_DIR "/POCSAG.TXT"); logger->append(LOG_ROOT_DIR "/POCSAG.TXT");
audio::output::start(); audio::output::start();
baseband::set_pocsag(); baseband::set_pocsag();
} }
@ -99,8 +100,9 @@ void POCSAGAppView::focus() {
POCSAGAppView::~POCSAGAppView() { POCSAGAppView::~POCSAGAppView() {
audio::output::stop(); audio::output::stop();
// Save ignored address // Save settings.
persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32()); persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32());
enable_logging = check_log.value();
receiver_model.disable(); receiver_model.disable();
baseband::shutdown(); baseband::shutdown();

View File

@ -65,7 +65,14 @@ class POCSAGAppView : public View {
NavigationView& nav_; NavigationView& nav_;
RxRadioState radio_state_{}; RxRadioState radio_state_{};
app_settings::SettingsManager settings_{ app_settings::SettingsManager settings_{
"rx_pocsag", app_settings::Mode::RX}; "rx_pocsag",
app_settings::Mode::RX};
// Settings
bool enable_logging = false;
SettingsStore settings_store_{
"rx_pocsag_ui",
{{"enable_logging", &enable_logging}}};
uint32_t last_address = 0xFFFFFFFF; uint32_t last_address = 0xFFFFFFFF;
pocsag::POCSAGState pocsag_state{}; pocsag::POCSAGState pocsag_state{};

View File

@ -360,6 +360,8 @@ TextEditorView::TextEditorView(NavigationView& nav)
&text_size, &text_size,
}); });
viewer.set_font_zoom(enable_zoom);
viewer.on_select = [this]() { viewer.on_select = [this]() {
// Treat as if menu button was pressed. // Treat as if menu button was pressed.
if (button_menu.on_select) if (button_menu.on_select)
@ -382,7 +384,7 @@ TextEditorView::TextEditorView(NavigationView& nav)
}; };
menu.on_zoom() = [this]() { menu.on_zoom() = [this]() {
viewer.toggle_font_zoom(); enable_zoom = viewer.toggle_font_zoom();
refresh_ui(); refresh_ui();
hide_menu(true); hide_menu(true);
}; };

View File

@ -32,6 +32,7 @@
#include "ui_styles.hpp" #include "ui_styles.hpp"
#include "ui_widget.hpp" #include "ui_widget.hpp"
#include "app_settings.hpp"
#include "file_wrapper.hpp" #include "file_wrapper.hpp"
#include "optional.hpp" #include "optional.hpp"
@ -80,7 +81,10 @@ class TextViewer : public Widget {
const Style& style() { return *font_style; } const Style& style() { return *font_style; }
void set_font_zoom(bool zoom); void set_font_zoom(bool zoom);
void toggle_font_zoom() { set_font_zoom(!font_zoom); }; bool toggle_font_zoom() {
set_font_zoom(!font_zoom);
return font_zoom;
};
private: private:
bool font_zoom{}; bool font_zoom{};
@ -220,6 +224,12 @@ class TextEditorView : public View {
void on_show() override; void on_show() override;
private: private:
// Settings
bool enable_zoom = false;
SettingsStore settings_store_{
"notepad",
{{"enable_zoom", &enable_zoom}}};
static constexpr size_t max_edit_length = 1024; static constexpr size_t max_edit_length = 1024;
std::string edit_line_buffer_{}; std::string edit_line_buffer_{};

View File

@ -270,18 +270,18 @@ std::string to_freqman_string(const freqman_entry& entry) {
switch (entry.type) { switch (entry.type) {
case freqman_type::Single: case freqman_type::Single:
append_field("f", to_string_dec_uint64(entry.frequency_a)); append_field("f", to_string_dec_uint(entry.frequency_a));
break; break;
case freqman_type::Range: case freqman_type::Range:
append_field("a", to_string_dec_uint64(entry.frequency_a)); append_field("a", to_string_dec_uint(entry.frequency_a));
append_field("b", to_string_dec_uint64(entry.frequency_b)); append_field("b", to_string_dec_uint(entry.frequency_b));
if (is_valid(entry.step)) if (is_valid(entry.step))
append_field("s", freqman_entry_get_step_string_short(entry.step)); append_field("s", freqman_entry_get_step_string_short(entry.step));
break; break;
case freqman_type::HamRadio: case freqman_type::HamRadio:
append_field("r", to_string_dec_uint64(entry.frequency_a)); append_field("r", to_string_dec_uint(entry.frequency_a));
append_field("t", to_string_dec_uint64(entry.frequency_b)); append_field("t", to_string_dec_uint(entry.frequency_b));
if (is_valid(entry.tone)) if (is_valid(entry.tone))
append_field("c", tonekey::tone_key_value_string(entry.tone)); append_field("c", tonekey::tone_key_value_string(entry.tone));

View File

@ -59,30 +59,37 @@ static char* to_string_dec_uint_pad_internal(
return q; return q;
} }
template <typename Int> static char* to_string_dec_uint_internal(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) {
auto end = &buffer.back(); auto end = &buffer.back();
auto start = to_string_dec_uint_internal(end, n); auto start = to_string_dec_uint_internal(end, n);
length = end - start; length = end - start;
return start; return start;
} }
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length) { char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length); return to_string_dec_uint_internal(n, buffer, length);
} }
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length) { char* to_string_dec_int(int64_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length); bool negative = n < 0;
auto start = to_string_dec_uint(negative ? -n : n, buffer, length);
if (negative) {
*(--start) = '-';
++length;
}
return start;
} }
std::string to_string_dec_uint(uint32_t n) { std::string to_string_dec_int(int64_t n) {
StringFormatBuffer b{}; StringFormatBuffer b{};
size_t len{}; size_t len{};
char* str = to_string_dec_uint(n, b, len); char* str = to_string_dec_int(n, b, len);
return std::string(str, len); return std::string(str, len);
} }
std::string to_string_dec_uint64(uint64_t n) { std::string to_string_dec_uint(uint64_t n) {
StringFormatBuffer b{}; StringFormatBuffer b{};
size_t len{}; size_t len{};
char* str = to_string_dec_uint(n, b, len); char* str = to_string_dec_uint(n, b, len);
@ -194,14 +201,14 @@ std::string to_string_rounded_freq(const uint64_t f, int8_t precision) {
}; };
if (precision < 1) { if (precision < 1) {
final_str = to_string_dec_uint64(f / 1000000); final_str = to_string_dec_uint(f / 1000000);
} else { } else {
if (precision > 6) if (precision > 6)
precision = 6; precision = 6;
uint32_t divisor = pow10[6 - precision]; 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'); final_str = to_string_dec_uint(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
} }
return final_str; return final_str;
} }

View File

@ -43,17 +43,17 @@ const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'};
using StringFormatBuffer = std::array<char, 24>; using StringFormatBuffer = std::array<char, 24>;
/* uint conversion without memory allocations. */ /* Integer conversion without memory allocations. */
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length); char* to_string_dec_int(int64_t n, StringFormatBuffer& buffer, size_t& length);
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length); char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length);
std::string to_string_dec_uint(uint32_t n); std::string to_string_dec_int(int64_t n);
std::string to_string_dec_uint64(uint64_t n); std::string to_string_dec_uint(uint64_t n);
// TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp... // TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
std::string to_string_bin(const uint32_t n, const uint8_t l = 0); std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' '); std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l = 0, const char fill = 0); std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision); 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(const uint64_t n, const int32_t l = 0);

View File

@ -99,4 +99,10 @@ TEST_CASE("It should convert 8-bit.") {
CHECK_EQ(val, 123); CHECK_EQ(val, 123);
} }
TEST_CASE("It should convert negative.") {
int8_t val = 0;
REQUIRE(parse_int("-64", val));
CHECK_EQ(val, -64);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -24,12 +24,27 @@
/* TODO: Tests for all string_format functions. */ /* TODO: Tests for all string_format functions. */
TEST_CASE("to_string_dec_uint64 returns correct value.") { TEST_CASE("to_string_dec_int returns correct value.") {
CHECK_EQ(to_string_dec_uint64(0), "0"); CHECK_EQ(to_string_dec_int(0), "0");
CHECK_EQ(to_string_dec_uint64(1), "1"); CHECK_EQ(to_string_dec_int(1), "1");
CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000"); CHECK_EQ(to_string_dec_int(-1), "-1");
CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890"); CHECK_EQ(to_string_dec_int(1'000'000), "1000000");
CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891"); CHECK_EQ(to_string_dec_int(-1'000'000), "-1000000");
CHECK_EQ(to_string_dec_int(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_int(-1'234'567'890), "-1234567890");
CHECK_EQ(to_string_dec_int(1'234'567'891), "1234567891");
CHECK_EQ(to_string_dec_int(-1'234'567'891), "-1234567891");
CHECK_EQ(to_string_dec_int(9'876'543'210), "9876543210");
CHECK_EQ(to_string_dec_int(-9'876'543'210), "-9876543210");
}
TEST_CASE("to_string_dec_uint returns correct value.") {
CHECK_EQ(to_string_dec_uint(0), "0");
CHECK_EQ(to_string_dec_uint(1), "1");
CHECK_EQ(to_string_dec_uint(1'000'000), "1000000");
CHECK_EQ(to_string_dec_uint(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_uint(1'234'567'891), "1234567891");
CHECK_EQ(to_string_dec_uint(9'876'543'210), "9876543210");
} }
TEST_CASE("to_string_freq returns correct value.") { TEST_CASE("to_string_freq returns correct value.") {