diff --git a/firmware/application/app_settings.cpp b/firmware/application/app_settings.cpp index 40d75f0b..624fec7d 100644 --- a/firmware/application/app_settings.cpp +++ b/firmware/application/app_settings.cpp @@ -23,7 +23,9 @@ #include "app_settings.hpp" +#include "convert.hpp" #include "file.hpp" +#include "file_reader.hpp" #include "portapack.hpp" #include "portapack_persistent_memory.hpp" #include "utility.hpp" @@ -36,6 +38,126 @@ namespace fs = std::filesystem; using namespace portapack; 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()); + break; + case SettingType::I32: + parse_int(value, as()); + break; + case SettingType::U32: + parse_int(value, as()); + break; + case SettingType::U8: + parse_int(value, as()); + break; + case SettingType::String: + as() = std::string{value}; + break; + case SettingType::Bool: { + int parsed = 0; + parse_int(value, parsed); + as() = (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(), buffer, length), length); + break; + case SettingType::I32: + file.write(to_string_dec_int(as(), buffer, length), length); + break; + case SettingType::U32: + file.write(to_string_dec_uint(as(), buffer, length), length); + break; + case SettingType::U8: + file.write(to_string_dec_uint(as(), buffer, length), length); + break; + case SettingType::String: { + const auto& str = as(); + file.write(str.data(), str.length()); + break; + } + case SettingType::Bool: + file.write(as() ? "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 { template @@ -64,10 +186,6 @@ static void write_setting(File& file, std::string_view setting_name, const T& va 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 { constexpr std::string_view baseband_bandwidth = "baseband_bandwidth="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; } // 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) { if (!portapack::persistent_memory::load_app_settings()) return ResultCode::SettingsDisabled; diff --git a/firmware/application/app_settings.hpp b/firmware/application/app_settings.hpp index b92f6476..276315d5 100644 --- a/firmware/application/app_settings.hpp +++ b/firmware/application/app_settings.hpp @@ -2,6 +2,7 @@ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek * Copyright (C) 2022 Arjan Onwezen + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -27,12 +28,77 @@ #include #include #include +#include #include +#include #include "file.hpp" #include "max283x.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 + constexpr auto& as() const { + return *reinterpret_cast(target_); + } + + std::string_view name_; + void* target_; + SettingType type_; +}; + +using SettingBindings = std::vector; + +/* 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 { enum class ResultCode : uint8_t { diff --git a/firmware/application/apps/pocsag_app.cpp b/firmware/application/apps/pocsag_app.cpp index 5180e1ec..1967384f 100644 --- a/firmware/application/apps/pocsag_app.cpp +++ b/firmware/application/apps/pocsag_app.cpp @@ -71,6 +71,8 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav) if (!settings_.loaded()) field_frequency.set_value(initial_target_frequency); + check_log.set_value(enable_logging); + receiver_model.enable(); // TODO: app setting instead? @@ -88,7 +90,6 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav) logger->append(LOG_ROOT_DIR "/POCSAG.TXT"); audio::output::start(); - baseband::set_pocsag(); } @@ -99,8 +100,9 @@ void POCSAGAppView::focus() { POCSAGAppView::~POCSAGAppView() { audio::output::stop(); - // Save ignored address + // Save settings. persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32()); + enable_logging = check_log.value(); receiver_model.disable(); baseband::shutdown(); diff --git a/firmware/application/apps/pocsag_app.hpp b/firmware/application/apps/pocsag_app.hpp index f43094b6..64fdda34 100644 --- a/firmware/application/apps/pocsag_app.hpp +++ b/firmware/application/apps/pocsag_app.hpp @@ -65,7 +65,14 @@ class POCSAGAppView : public View { NavigationView& nav_; RxRadioState radio_state_{}; 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; pocsag::POCSAGState pocsag_state{}; diff --git a/firmware/application/apps/ui_text_editor.cpp b/firmware/application/apps/ui_text_editor.cpp index 892cadb9..130f76c9 100644 --- a/firmware/application/apps/ui_text_editor.cpp +++ b/firmware/application/apps/ui_text_editor.cpp @@ -360,6 +360,8 @@ TextEditorView::TextEditorView(NavigationView& nav) &text_size, }); + viewer.set_font_zoom(enable_zoom); + viewer.on_select = [this]() { // Treat as if menu button was pressed. if (button_menu.on_select) @@ -382,7 +384,7 @@ TextEditorView::TextEditorView(NavigationView& nav) }; menu.on_zoom() = [this]() { - viewer.toggle_font_zoom(); + enable_zoom = viewer.toggle_font_zoom(); refresh_ui(); hide_menu(true); }; diff --git a/firmware/application/apps/ui_text_editor.hpp b/firmware/application/apps/ui_text_editor.hpp index 7a7bec83..f2b97e52 100644 --- a/firmware/application/apps/ui_text_editor.hpp +++ b/firmware/application/apps/ui_text_editor.hpp @@ -32,6 +32,7 @@ #include "ui_styles.hpp" #include "ui_widget.hpp" +#include "app_settings.hpp" #include "file_wrapper.hpp" #include "optional.hpp" @@ -80,7 +81,10 @@ class TextViewer : public Widget { const Style& style() { return *font_style; } 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: bool font_zoom{}; @@ -220,6 +224,12 @@ class TextEditorView : public View { void on_show() override; private: + // Settings + bool enable_zoom = false; + SettingsStore settings_store_{ + "notepad", + {{"enable_zoom", &enable_zoom}}}; + static constexpr size_t max_edit_length = 1024; std::string edit_line_buffer_{}; diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp index 22075160..e32599ac 100644 --- a/firmware/application/freqman_db.cpp +++ b/firmware/application/freqman_db.cpp @@ -270,18 +270,18 @@ std::string to_freqman_string(const freqman_entry& entry) { switch (entry.type) { 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; case freqman_type::Range: - append_field("a", to_string_dec_uint64(entry.frequency_a)); - append_field("b", to_string_dec_uint64(entry.frequency_b)); + append_field("a", to_string_dec_uint(entry.frequency_a)); + append_field("b", to_string_dec_uint(entry.frequency_b)); if (is_valid(entry.step)) append_field("s", freqman_entry_get_step_string_short(entry.step)); break; case freqman_type::HamRadio: - append_field("r", to_string_dec_uint64(entry.frequency_a)); - append_field("t", to_string_dec_uint64(entry.frequency_b)); + append_field("r", to_string_dec_uint(entry.frequency_a)); + append_field("t", to_string_dec_uint(entry.frequency_b)); if (is_valid(entry.tone)) append_field("c", tonekey::tone_key_value_string(entry.tone)); diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index aac7add7..a63b0167 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -59,30 +59,37 @@ static char* to_string_dec_uint_pad_internal( return q; } -template -char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) { +static char* to_string_dec_uint_internal(uint64_t n, StringFormatBuffer& buffer, size_t& length) { auto end = &buffer.back(); auto start = to_string_dec_uint_internal(end, n); length = end - 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); } -char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length) { - return to_string_dec_uint_internal(n, buffer, length); +char* to_string_dec_int(int64_t n, StringFormatBuffer& buffer, size_t& 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{}; 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); } -std::string to_string_dec_uint64(uint64_t n) { +std::string to_string_dec_uint(uint64_t n) { StringFormatBuffer b{}; size_t 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) { - final_str = to_string_dec_uint64(f / 1000000); + final_str = to_string_dec_uint(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'); + final_str = to_string_dec_uint(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0'); } return final_str; } diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index a5f26501..98476fce 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -43,17 +43,17 @@ const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'}; using StringFormatBuffer = std::array; -/* uint conversion without memory allocations. */ -char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length); -char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length); +/* Integer conversion without memory allocations. */ +char* to_string_dec_int(int64_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_uint64(uint64_t n); +std::string to_string_dec_int(int64_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... 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_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_hex(const uint64_t n, const int32_t l = 0); diff --git a/firmware/test/application/test_convert.cpp b/firmware/test/application/test_convert.cpp index 30f912c3..793538b5 100644 --- a/firmware/test/application/test_convert.cpp +++ b/firmware/test/application/test_convert.cpp @@ -99,4 +99,10 @@ TEST_CASE("It should convert 8-bit.") { 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(); \ No newline at end of file diff --git a/firmware/test/application/test_string_format.cpp b/firmware/test/application/test_string_format.cpp index a9ac8e50..456efc20 100644 --- a/firmware/test/application/test_string_format.cpp +++ b/firmware/test/application/test_string_format.cpp @@ -24,12 +24,27 @@ /* TODO: Tests for all string_format functions. */ -TEST_CASE("to_string_dec_uint64 returns correct value.") { - CHECK_EQ(to_string_dec_uint64(0), "0"); - CHECK_EQ(to_string_dec_uint64(1), "1"); - CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000"); - CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890"); - CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891"); +TEST_CASE("to_string_dec_int returns correct value.") { + CHECK_EQ(to_string_dec_int(0), "0"); + CHECK_EQ(to_string_dec_int(1), "1"); + CHECK_EQ(to_string_dec_int(-1), "-1"); + CHECK_EQ(to_string_dec_int(1'000'000), "1000000"); + 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.") {