From 497ca3f934c3f3e27ff6a9da45b05fc98ce00cfc Mon Sep 17 00:00:00 2001 From: Kyle Reed <3761006+kallanreed@users.noreply.github.com> Date: Sat, 8 Jul 2023 13:04:12 -0700 Subject: [PATCH] Refactor freqman_db parsing (#1244) * WIP freqman changes/memory perf/stash * Split ui tone_key function out for testing. * Add more tests and fix bugs. * Use default max_entries in recond * Set limit back to 90 for now --- firmware/application/CMakeLists.txt | 2 + firmware/application/apps/soundboard_app.cpp | 1 + firmware/application/apps/soundboard_app.hpp | 1 - firmware/application/apps/ui_freqman.cpp | 56 ++- firmware/application/apps/ui_freqman.hpp | 1 - firmware/application/apps/ui_level.cpp | 1 + firmware/application/apps/ui_mictx.cpp | 13 +- firmware/application/apps/ui_mictx.hpp | 1 - firmware/application/apps/ui_recon.cpp | 296 ++++++------- firmware/application/apps/ui_recon.hpp | 5 + firmware/application/apps/ui_scanner.cpp | 104 ++--- firmware/application/apps/ui_scanner.hpp | 2 +- firmware/application/file_reader.hpp | 17 +- firmware/application/freqman.cpp | 413 +++--------------- firmware/application/freqman.hpp | 76 +--- firmware/application/freqman_db.cpp | 264 +++++++++++ firmware/application/freqman_db.hpp | 179 ++++++++ firmware/application/string_format.cpp | 2 +- firmware/application/tone_key.cpp | 23 +- firmware/application/tone_key.hpp | 12 +- firmware/application/ui/ui_freqlist.cpp | 2 +- firmware/application/ui/ui_receiver.cpp | 2 +- firmware/application/ui/ui_tone_key.cpp | 47 ++ firmware/application/ui/ui_tone_key.hpp | 36 ++ firmware/common/ui_widget.cpp | 16 +- firmware/common/ui_widget.hpp | 8 +- firmware/test/application/CMakeLists.txt | 8 + firmware/test/application/linker_stubs.cpp | 80 ++++ .../test/application/test_file_reader.cpp | 26 ++ firmware/test/application/test_freqman_db.cpp | 217 +++++++++ 30 files changed, 1206 insertions(+), 705 deletions(-) create mode 100644 firmware/application/freqman_db.cpp create mode 100644 firmware/application/freqman_db.hpp create mode 100644 firmware/application/ui/ui_tone_key.cpp create mode 100644 firmware/application/ui/ui_tone_key.hpp create mode 100644 firmware/test/application/linker_stubs.cpp create mode 100644 firmware/test/application/test_freqman_db.cpp diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 4c50ccfe..2b64c292 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -177,6 +177,7 @@ set(CPPSRC event_m0.cpp file_reader.cpp file.cpp + freqman_db.cpp freqman.cpp io_file.cpp io_wave.cpp @@ -234,6 +235,7 @@ set(CPPSRC ui/ui_styles.cpp ui/ui_tabview.cpp ui/ui_textentry.cpp + ui/ui_tone_key.cpp ui/ui_transmitter.cpp apps/ui_about_simple.cpp apps/ui_adsb_rx.cpp diff --git a/firmware/application/apps/soundboard_app.cpp b/firmware/application/apps/soundboard_app.cpp index b5f50c83..2431e6c7 100644 --- a/firmware/application/apps/soundboard_app.cpp +++ b/firmware/application/apps/soundboard_app.cpp @@ -25,6 +25,7 @@ #include "soundboard_app.hpp" #include "string_format.hpp" #include "tonesets.hpp" +#include "ui_tone_key.hpp" using namespace tonekey; using namespace portapack; diff --git a/firmware/application/apps/soundboard_app.hpp b/firmware/application/apps/soundboard_app.hpp index 44995c71..08708f5b 100644 --- a/firmware/application/apps/soundboard_app.hpp +++ b/firmware/application/apps/soundboard_app.hpp @@ -29,7 +29,6 @@ #include "baseband_api.hpp" #include "lfsr_random.hpp" #include "io_wave.hpp" -#include "tone_key.hpp" #include "app_settings.hpp" #include "radio_state.hpp" diff --git a/firmware/application/apps/ui_freqman.cpp b/firmware/application/apps/ui_freqman.cpp index 75c26fca..9c7cad38 100644 --- a/firmware/application/apps/ui_freqman.cpp +++ b/firmware/application/apps/ui_freqman.cpp @@ -25,6 +25,8 @@ #include "portapack.hpp" #include "event_m0.hpp" +#include + using namespace portapack; namespace ui { @@ -63,7 +65,8 @@ void FreqManBaseView::focus() { } void FreqManBaseView::get_freqman_files() { - std::vector().swap(file_list); + // Assume this does change much, clear will preserve the existing alloc. + file_list.clear(); auto files = scan_root_files(u"FREQMAN", u"*.TXT"); @@ -71,7 +74,7 @@ void FreqManBaseView::get_freqman_files() { std::string file_name = file.stem().string(); // don't propose tmp / hidden files in freqman's list if (file_name.length() && file_name[0] != '.') { - file_list.emplace_back(file_name); + file_list.emplace_back(std::move(file_name)); } } }; @@ -81,7 +84,7 @@ void FreqManBaseView::change_category(int32_t category_id) { if (file_list.empty()) return; - if (!load_freqman_file(file_list[categories[category_id].second], database)) { + if (!load_freqman_file(file_list[categories[category_id].second], database, {})) { error_ = ERROR_ACCESS; } freqlist_view.set_db(database); @@ -113,13 +116,13 @@ void FrequencySaveView::save_current_file() { void FrequencySaveView::on_save_name() { text_prompt(nav_, desc_buffer, 28, [this](std::string& buffer) { - database.push_back({value_, 0, buffer, SINGLE}); + database.push_back(std::make_unique(freqman_entry{value_, 0, buffer, freqman_type::Single})); save_current_file(); }); } void FrequencySaveView::on_save_timestamp() { - database.push_back({value_, 0, live_timestamp.string(), SINGLE}); + database.push_back(std::make_unique(freqman_entry{value_, 0, live_timestamp.string(), freqman_type::Single})); save_current_file(); } @@ -159,10 +162,6 @@ FrequencySaveView::FrequencySaveView( }; } -FrequencyLoadView::~FrequencyLoadView() { - std::vector().swap(database); -} - void FrequencyLoadView::refresh_widgets(const bool v) { freqlist_view.hidden(v); text_empty.hidden(!v); @@ -185,33 +184,32 @@ FrequencyLoadView::FrequencyLoadView( freqlist_view.on_select = [&nav, this](FreqManUIList&) { auto& entry = database[freqlist_view.get_index()]; - if (entry.type == RANGE) { - // User chose a frequency range entry + if (entry->type == freqman_type::Range) { if (on_range_loaded) - on_range_loaded(entry.frequency_a, entry.frequency_b); + on_range_loaded(entry->frequency_a, entry->frequency_b); else if (on_frequency_loaded) - on_frequency_loaded(entry.frequency_a); - // TODO: Maybe return center of range if user choses a range when the app needs a unique frequency, instead of frequency_a ? + on_frequency_loaded(entry->frequency_a); + // TODO: Maybe return center of range if user choses a range when the app + // needs a unique frequency, instead of frequency_a? + // TODO: HamRadio? } else { - // User chose an unique frequency entry if (on_frequency_loaded) - on_frequency_loaded(entry.frequency_a); + on_frequency_loaded(entry->frequency_a); } - // swap with empty vector to ensure memory is immediately released - std::vector().swap(database); - nav_.pop(); + + nav_.pop(); // NB: this will call dtor. }; } void FrequencyManagerView::on_edit_freq(rf::Frequency f) { - database[freqlist_view.get_index()].frequency_a = f; + database[freqlist_view.get_index()]->frequency_a = f; save_freqman_file(file_list[categories[current_category_id].second], database); change_category(current_category_id); } void FrequencyManagerView::on_edit_desc(NavigationView& nav) { text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) { - database[freqlist_view.get_index()].description = buffer; + database[freqlist_view.get_index()]->description = std::move(buffer); save_freqman_file(file_list[categories[current_category_id].second], database); change_category(current_category_id); }); @@ -277,20 +275,20 @@ FrequencyManagerView::FrequencyManagerView( }; button_edit_freq.on_select = [this, &nav](Button&) { - if (database.empty()) { - database.push_back({0, 0, "", SINGLE}); - } - auto new_view = nav.push(database[freqlist_view.get_index()].frequency_a); + if (database.empty()) + database.push_back(std::make_unique(freqman_entry{0, 0, "", freqman_type::Single})); + + auto new_view = nav.push(database[freqlist_view.get_index()]->frequency_a); new_view->on_changed = [this](rf::Frequency f) { on_edit_freq(f); }; }; button_edit_desc.on_select = [this, &nav](Button&) { - if (database.empty()) { - database.push_back({0, 0, "", SINGLE}); - } - desc_buffer = database[freqlist_view.get_index()].description; + if (database.empty()) + database.push_back(std::make_unique(freqman_entry{0, 0, "", freqman_type::Single})); + + desc_buffer = database[freqlist_view.get_index()]->description; on_edit_desc(nav); }; diff --git a/firmware/application/apps/ui_freqman.hpp b/firmware/application/apps/ui_freqman.hpp index 0c1eeea3..be54e467 100644 --- a/firmware/application/apps/ui_freqman.hpp +++ b/firmware/application/apps/ui_freqman.hpp @@ -116,7 +116,6 @@ class FrequencyLoadView : public FreqManBaseView { std::function on_range_loaded{}; FrequencyLoadView(NavigationView& nav); - ~FrequencyLoadView(); std::string title() const override { return "Load freq."; }; diff --git a/firmware/application/apps/ui_level.cpp b/firmware/application/apps/ui_level.cpp index 98de133b..68c294e1 100644 --- a/firmware/application/apps/ui_level.cpp +++ b/firmware/application/apps/ui_level.cpp @@ -26,6 +26,7 @@ #include "file.hpp" using namespace portapack; +using namespace tonekey; using portapack::memory::map::backup_ram; namespace ui { diff --git a/firmware/application/apps/ui_mictx.cpp b/firmware/application/apps/ui_mictx.cpp index 3f694654..c001e0f0 100644 --- a/firmware/application/apps/ui_mictx.cpp +++ b/firmware/application/apps/ui_mictx.cpp @@ -22,21 +22,20 @@ #include "ui_mictx.hpp" -#include "baseband_api.hpp" #include "audio.hpp" - -#include "wm8731.hpp" -using wolfson::wm8731::WM8731; - -#include "tonesets.hpp" +#include "baseband_api.hpp" +#include "irq_controls.hpp" #include "portapack_hal.hpp" #include "string_format.hpp" -#include "irq_controls.hpp" +#include "tonesets.hpp" +#include "ui_tone_key.hpp" +#include "wm8731.hpp" #include using namespace tonekey; using namespace portapack; +using wolfson::wm8731::WM8731; WM8731 audio_codec_wm8731{i2c0, 0x1a}; diff --git a/firmware/application/apps/ui_mictx.hpp b/firmware/application/apps/ui_mictx.hpp index cca65e67..6b7640de 100644 --- a/firmware/application/apps/ui_mictx.hpp +++ b/firmware/application/apps/ui_mictx.hpp @@ -35,7 +35,6 @@ #include "ui_navigation.hpp" #include "ui_receiver.hpp" #include "transmitter_model.hpp" -#include "tone_key.hpp" #include "message.hpp" #include "receiver_model.hpp" #include "ui_transmitter.hpp" diff --git a/firmware/application/apps/ui_recon.cpp b/firmware/application/apps/ui_recon.cpp index bdc7e75f..4ab4966b 100644 --- a/firmware/application/apps/ui_recon.cpp +++ b/firmware/application/apps/ui_recon.cpp @@ -23,14 +23,24 @@ #include "ui_recon.hpp" #include "ui_fileman.hpp" -#include "file.hpp" #include "capture_app.hpp" +#include "file.hpp" +#include "tone_key.hpp" using namespace portapack; +using namespace tonekey; using portapack::memory::map::backup_ram; namespace ui { +bool ReconView::current_is_valid() { + return (unsigned)current_index < frequency_list.size(); +} + +freqman_entry& ReconView::current_entry() { + return *frequency_list[current_index]; +} + void ReconView::set_loop_config(bool v) { continuous = v; button_loop_config.set_style(v ? &Styles::green : &Styles::white); @@ -56,22 +66,41 @@ void ReconView::clear_freqlist_for_ui_action() { audio::output::stop(); // flag to detect and reload frequency_list if (!manual_mode) { - // clear and shrink_to_fit are not enough to really start with a new, clean, empty vector - // swap is the only way to achieve a perfect memory liberation - std::vector().swap(frequency_list); + // Clear doesn't actually free, re-assign so destructor runs on previous instance. + frequency_list = freqman_db{}; } else frequency_list.shrink_to_fit(); freqlist_cleared_for_ui_action = true; } void ReconView::reset_indexes() { - last_entry.modulation = -1; - last_entry.bandwidth = -1; - last_entry.step = -1; - description = "...no description..."; + last_entry.modulation = freqman_invalid_index; + last_entry.bandwidth = freqman_invalid_index; + last_entry.step = freqman_invalid_index; current_index = 0; } +void ReconView::update_description() { + if (frequency_list.empty() || current_entry().description.empty()) { + description = "...no description..."; + return; + } else { + switch (current_entry().type) { + case freqman_type::Range: + description = "R: "; + break; + case freqman_type::HamRadio: + description = "H: "; + break; + default: + description = "S: "; + } + description += current_entry().description; + } + + desc_cycle.set(description); +} + void ReconView::colorize_waits() { // colorize wait on match if (wait == 0) { @@ -98,15 +127,15 @@ 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; - if (frequency_list.size() == 0 || (frequency_list.size() && current_index > (int32_t)frequency_list.size())) + if (frequency_list.size() == 0 || !current_is_valid()) return false; - freqman_entry entry = frequency_list[freq_index]; + 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 = SINGLE; + entry.type = freqman_type::Single; std::string frequency_to_add; get_freq_string(entry, frequency_to_add); @@ -176,7 +205,7 @@ bool ReconView::recon_load_config_from_sd() { pos = line_start; while ((line_end = strstr(line_start, "\x0A"))) { length = line_end - line_start - 1; - params[it] = string(pos, length); + params[it] = std::string(pos, length); it++; line_start = line_end + 1; pos = line_start; @@ -311,53 +340,38 @@ void ReconView::handle_retune() { receiver_model.set_target_frequency(freq); // Retune } if (frequency_list.size() > 0) { - if (last_entry.modulation != frequency_list[current_index].modulation && frequency_list[current_index].modulation >= 0) { - last_entry.modulation = frequency_list[current_index].modulation; - field_mode.set_selected_index(frequency_list[current_index].modulation); - last_entry.bandwidth = -1; + if (last_entry.modulation != current_entry().modulation && is_valid(current_entry().modulation)) { + last_entry.modulation = current_entry().modulation; + field_mode.set_selected_index(current_entry().modulation); + last_entry.bandwidth = freqman_invalid_index; } // Set bandwidth if any - if (last_entry.bandwidth != frequency_list[current_index].bandwidth && frequency_list[current_index].bandwidth >= 0) { - last_entry.bandwidth = frequency_list[current_index].bandwidth; - field_bw.set_selected_index(frequency_list[current_index].bandwidth); + if (last_entry.bandwidth != current_entry().bandwidth && is_valid(current_entry().bandwidth)) { + last_entry.bandwidth = current_entry().bandwidth; + field_bw.set_selected_index(current_entry().bandwidth); } - if (last_entry.step != frequency_list[current_index].step && frequency_list[current_index].step >= 0) { - last_entry.step = frequency_list[current_index].step; + if (last_entry.step != current_entry().step && is_valid(current_entry().step)) { + last_entry.step = current_entry().step; step = freqman_entry_get_step_value(last_entry.step); step_mode.set_selected_index(step); } if (last_index != current_index) { last_index = current_index; - if ((int32_t)frequency_list.size() && current_index < (int32_t)frequency_list.size() && frequency_list[current_index].type == RANGE) { + if (frequency_list.size() && current_is_valid() && current_entry().type == freqman_type::Range) { if (update_ranges && !manual_mode) { - button_manual_start.set_text(to_string_short_freq(frequency_list[current_index].frequency_a)); - frequency_range.min = frequency_list[current_index].frequency_a; - if (frequency_list[current_index].frequency_b != 0) { - button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_b)); - frequency_range.max = frequency_list[current_index].frequency_b; + button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a)); + frequency_range.min = current_entry().frequency_a; + if (current_entry().frequency_b != 0) { + button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b)); + frequency_range.max = current_entry().frequency_b; } else { - button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_a)); - frequency_range.max = frequency_list[current_index].frequency_a; + button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a)); + frequency_range.max = current_entry().frequency_a; } } } text_cycle.set_text(to_string_dec_uint(current_index + 1, 3)); - if (frequency_list[current_index].description.size() > 0) { - switch (frequency_list[current_index].type) { - case RANGE: - desc_cycle.set("R: " + frequency_list[current_index].description); // Show new description - break; - case HAMRADIO: - desc_cycle.set("H: " + frequency_list[current_index].description); // Show new description - break; - default: - case SINGLE: - desc_cycle.set("S: " + frequency_list[current_index].description); // Show new description - break; - } - } else { - desc_cycle.set("...no description..."); // Show new description - } + update_description(); } } } @@ -576,11 +590,11 @@ ReconView::ReconView(NavigationView& nav) // TODO: *BUG* Both transmitter_model and receiver_model share the same pmem setting for target_frequency. button_mic_app.on_select = [this](Button&) { if (frequency_list.size() > 0 && current_index >= 0 && (unsigned)current_index < frequency_list.size()) { - if (frequency_list[current_index].type == HAMRADIO) { - // if it's a HAMRADIO entry, then frequency_a is the freq at which the repeater receives, so we have to set it in transmit in mic app - transmitter_model.set_target_frequency(frequency_list[current_index].frequency_a); - // if it's a HAMRADIO entry, then frequency_b is the freq at which the repeater transmits, so we have to set it in receive in mic app - receiver_model.set_target_frequency(frequency_list[current_index].frequency_b); + if (current_entry().type == freqman_type::HamRadio) { + // if it's a HamRadio entry, then frequency_a is the freq at which the repeater receives, so we have to set it in transmit in mic app + transmitter_model.set_target_frequency(current_entry().frequency_a); + // if it's a HamRadio entry, then frequency_b is the freq at which the repeater transmits, so we have to set it in receive in mic app + receiver_model.set_target_frequency(current_entry().frequency_b); } else { // it's single or range so we us actual tuned frequency transmitter_model.set_target_frequency(freq); @@ -605,23 +619,8 @@ ReconView::ReconView(NavigationView& nav) current_index = frequency_list.size() - 1; } if (frequency_list.size() > 0) { - if (frequency_list[current_index].description.size() > 0) { - switch (frequency_list[current_index].type) { - case RANGE: - desc_cycle.set("R: " + frequency_list[current_index].description); - break; - case HAMRADIO: - desc_cycle.set("H: " + frequency_list[current_index].description); - break; - default: - case SINGLE: - desc_cycle.set("S: " + frequency_list[current_index].description); - break; - } - } else { - desc_cycle.set("...no description..."); - } 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) { @@ -631,7 +630,7 @@ ReconView::ReconView(NavigationView& nav) if (!result.is_valid()) { for (size_t n = 0; n < frequency_list.size(); n++) { std::string line; - get_freq_string(frequency_list[n], line); + get_freq_string(*frequency_list[n], line); freqman_file.write_line(line); } } @@ -643,12 +642,12 @@ ReconView::ReconView(NavigationView& nav) std::string tmp_freq_file_path{freq_file_path + ".TMP"}; std::string frequency_to_add{}; - freqman_entry entry = frequency_list[current_index]; + 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 = SINGLE; + entry.type = freqman_type::Single; get_freq_string(entry, frequency_to_add); @@ -683,7 +682,7 @@ ReconView::ReconView(NavigationView& nav) } } } - receiver_model.set_target_frequency(frequency_list[current_index].frequency_a); // retune + receiver_model.set_target_frequency(current_entry().frequency_a); // retune } if (frequency_list.size() == 0) { text_cycle.set_text(" "); @@ -711,25 +710,23 @@ ReconView::ReconView(NavigationView& nav) } else { if (field_mode.selected_index_value() != SPEC_MODULATION) audio::output::stop(); - // clear and shrink_to_fit are not enough to really start with a new, clean, empty vector - // swap is the only way to achieve a perfect memory liberation - std::vector().swap(frequency_list); - freqman_entry manual_freq_entry; + // Clear doesn't actually free, re-assign so destructor runs on previous instance. + frequency_list = freqman_db{}; + current_index = 0; + frequency_list.push_back(std::make_unique()); - def_step = step_mode.selected_index(); // max range val - - manual_freq_entry.type = RANGE; - manual_freq_entry.description = - to_string_short_freq(frequency_range.min).erase(0, 1) + ">" + to_string_short_freq(frequency_range.max).erase(0, 1) + " S:" // current Manual range - + freqman_entry_get_step_string_short(def_step); // euquiq: lame kludge to reduce spacing in step freq - manual_freq_entry.frequency_a = frequency_range.min; // min range val - manual_freq_entry.frequency_b = frequency_range.max; // max range val - manual_freq_entry.modulation = -1; - manual_freq_entry.bandwidth = -1; - manual_freq_entry.step = def_step; - - frequency_list.push_back(manual_freq_entry); + def_step = step_mode.selected_index(); + current_entry().type = freqman_type::Range; + current_entry().description = + to_string_short_freq(frequency_range.min).erase(0, 1) + ">" + // euquiq: lame kludge to reduce spacing in step freq + to_string_short_freq(frequency_range.max).erase(0, 1) + " S:" + + freqman_entry_get_step_string_short(def_step); + current_entry().frequency_a = frequency_range.min; + current_entry().frequency_b = frequency_range.max; + current_entry().modulation = freqman_invalid_index; + current_entry().bandwidth = freqman_invalid_index; + current_entry().step = def_step; big_display.set_style(&Styles::white); // Back to white color @@ -744,13 +741,12 @@ ReconView::ReconView(NavigationView& nav) file_name.set("MANUAL RANGE RECON"); desc_cycle.set_style(&Styles::white); - last_entry.modulation = -1; - last_entry.bandwidth = -1; - last_entry.step = -1; + last_entry.modulation = freqman_invalid_index; + last_entry.bandwidth = freqman_invalid_index; + last_entry.step = freqman_invalid_index; last_index = -1; - current_index = 0; - freq = manual_freq_entry.frequency_a; + freq = current_entry().frequency_a; handle_retune(); recon_redraw(); recon_resume(); @@ -795,7 +791,7 @@ ReconView::ReconView(NavigationView& nav) } }; - button_add.on_select = [this](ButtonWithEncoder&) { // frequency_list[current_index] + button_add.on_select = [this](ButtonWithEncoder&) { if (!scanner_mode) { recon_save_freq(freq_file_path, current_index, true); } @@ -945,7 +941,11 @@ void ReconView::frequency_file_load(bool stop_all_before) { desc_cycle.set_style(&Styles::blue); button_scanner_mode.set_text("RECON"); } - if (!load_freqman_file(file_input, frequency_list, load_freqs, load_ranges, load_hamradios)) { + freqman_load_options options{ + .load_freqs = load_freqs, + .load_ranges = load_ranges, + .load_hamradios = load_hamradios}; + if (!load_freqman_file(file_input, frequency_list, options)) { file_name.set_style(&Styles::red); desc_cycle.set(" NO " + file_input + ".TXT FILE ..."); file_name.set("=> NO DATA"); @@ -962,71 +962,39 @@ void ReconView::frequency_file_load(bool stop_all_before) { } } - if (frequency_list[0].step >= 0) - step = freqman_entry_get_step_value(frequency_list[0].step); - else - step = freqman_entry_get_step_value(def_step); - - switch (frequency_list[0].type) { - case SINGLE: - freq = frequency_list[0].frequency_a; - break; - case RANGE: - minfreq = frequency_list[0].frequency_a; - maxfreq = frequency_list[0].frequency_b; - if (fwd) { - freq = minfreq; - } else { - freq = maxfreq; - } - if (frequency_list[0].step >= 0) - step = freqman_entry_get_step_value(frequency_list[0].step); - break; - case HAMRADIO: - minfreq = frequency_list[0].frequency_a; - maxfreq = frequency_list[0].frequency_b; - if (fwd) { - freq = minfreq; - } else { - freq = maxfreq; - } - break; - default: - break; + if (frequency_list.empty()) { + text_cycle.set_text(" "); + return; // Can't really do much. } + reset_indexes(); + step = freqman_entry_get_step_value( + is_valid(current_entry().step) ? current_entry().step : def_step); + + if (current_entry().type == freqman_type::Single) { + freq = current_entry().frequency_a; + } else if (current_entry().type != freqman_type::Unknown) { + minfreq = current_entry().frequency_a; + maxfreq = current_entry().frequency_b; + freq = fwd ? minfreq : maxfreq; + } + step_mode.set_selected_index(def_step); // Impose the default step into the manual step selector receiver_model.enable(); receiver_model.set_squelch_level(0); - if (frequency_list.size() != 0) { - switch (frequency_list[current_index].type) { - case RANGE: - description = "R: " + frequency_list[current_index].description; - break; - case HAMRADIO: - description = "H: " + frequency_list[current_index].description; - break; - default: - case SINGLE: - description = "S: " + frequency_list[current_index].description; - break; + text_cycle.set_text(to_string_dec_uint(current_index + 1, 3)); + if (update_ranges && !manual_mode) { + button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a)); + frequency_range.min = current_entry().frequency_a; + if (current_entry().frequency_b != 0) { + button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b)); + frequency_range.max = current_entry().frequency_b; + } else { + button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a)); + frequency_range.max = current_entry().frequency_a; } - text_cycle.set_text(to_string_dec_uint(current_index + 1, 3)); - if (update_ranges && !manual_mode) { - button_manual_start.set_text(to_string_short_freq(frequency_list[current_index].frequency_a)); - frequency_range.min = frequency_list[current_index].frequency_a; - if (frequency_list[current_index].frequency_b != 0) { - button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_b)); - frequency_range.max = frequency_list[current_index].frequency_b; - } else { - button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_a)); - frequency_range.max = frequency_list[current_index].frequency_a; - } - } - } else { - text_cycle.set_text(" "); } - desc_cycle.set(description); + update_description(); handle_retune(); } @@ -1156,7 +1124,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { if (recon || stepper != 0 || index_stepper != 0) { if (index_stepper == 0) { /* we are doing a range */ - if (frequency_list[current_index].type == RANGE) { + if (current_entry().type == freqman_type::Range) { if ((fwd && stepper == 0) || stepper > 0) { // forward freq += step; @@ -1186,7 +1154,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { } } } - } else if (frequency_list[current_index].type == SINGLE) { + } else if (current_entry().type == freqman_type::Single) { if ((fwd && stepper == 0) || stepper > 0) { // forward current_index++; entry_has_changed = true; @@ -1205,7 +1173,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { current_index = frequency_list.size() - 1; } } - } else if (frequency_list[current_index].type == HAMRADIO) { + } else if (current_entry().type == freqman_type::HamRadio) { if ((fwd && stepper == 0) || stepper > 0) { // forward if ((minfreq != maxfreq) && freq == minfreq) { freq = maxfreq; @@ -1259,22 +1227,22 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { // reload entry if changed if (entry_has_changed) { timer = 0; - switch (frequency_list[current_index].type) { - case SINGLE: - freq = frequency_list[current_index].frequency_a; + switch (current_entry().type) { + case freqman_type::Single: + freq = current_entry().frequency_a; break; - case RANGE: - minfreq = frequency_list[current_index].frequency_a; - maxfreq = frequency_list[current_index].frequency_b; + case freqman_type::Range: + minfreq = current_entry().frequency_a; + maxfreq = current_entry().frequency_b; if ((fwd && !stepper && !index_stepper) || stepper > 0 || index_stepper > 0) { freq = minfreq; } else if ((!fwd && !stepper && !index_stepper) || stepper < 0 || index_stepper < 0) { freq = maxfreq; } break; - case HAMRADIO: - minfreq = frequency_list[current_index].frequency_a; - maxfreq = frequency_list[current_index].frequency_b; + case freqman_type::HamRadio: + minfreq = current_entry().frequency_a; + maxfreq = current_entry().frequency_b; if ((fwd && !stepper && !index_stepper) || stepper > 0 || index_stepper > 0) { freq = minfreq; } else if ((!fwd && !stepper && !index_stepper) || stepper < 0 || index_stepper < 0) { diff --git a/firmware/application/apps/ui_recon.hpp b/firmware/application/apps/ui_recon.hpp index 6cd24bc4..df004249 100644 --- a/firmware/application/apps/ui_recon.hpp +++ b/firmware/application/apps/ui_recon.hpp @@ -67,6 +67,7 @@ class ReconView : public View { void set_loop_config(bool v); void clear_freqlist_for_ui_action(); void reset_indexes(); + void update_description(); void audio_output_start(); bool check_sd_card(); size_t change_mode(freqman_index_t mod_type); @@ -87,6 +88,10 @@ class ReconView : public View { // placeholder for possible void recon_start_recording(); void recon_stop_recording(); + // Returns true if 'current_index' is in bounds of frequency_list. + bool current_is_valid(); + freqman_entry& current_entry(); + jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too... int32_t squelch{0}; int32_t db{0}; diff --git a/firmware/application/apps/ui_scanner.cpp b/firmware/application/apps/ui_scanner.cpp index e545cf47..65fe50c1 100644 --- a/firmware/application/apps/ui_scanner.cpp +++ b/firmware/application/apps/ui_scanner.cpp @@ -318,7 +318,7 @@ ScannerView::ScannerView( 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 (str_file_path.find(dir_filter) != std::string::npos) { // assert file from the FREQMAN folder scan_pause(); // get the filename without txt extension so we can use load_freqman_file fcn std::string str_file_name = new_file_path.stem().string(); @@ -576,9 +576,9 @@ ScannerView::ScannerView( void ScannerView::frequency_file_load(std::string file_name, bool stop_all_before) { bool found_range{false}; bool found_single{false}; - freqman_index_t def_mod_index{-1}; - freqman_index_t def_bw_index{-1}; - freqman_index_t def_step_index{-1}; + freqman_index_t def_mod_index{freqman_invalid_index}; + freqman_index_t def_bw_index{freqman_invalid_index}; + freqman_index_t def_step_index{freqman_invalid_index}; // stop everything running now if required if (stop_all_before) { @@ -587,56 +587,56 @@ void ScannerView::frequency_file_load(std::string file_name, bool stop_all_befor description_list.clear(); } - if (load_freqman_file(file_name, database)) { - loaded_file_name = file_name; // keep loaded filename in memory - for (auto& entry : database) { // READ LINE PER LINE - if (frequency_list.size() < FREQMAN_MAX_PER_FILE) { // We got space! - // - // Get modulation & bw & step from file if specified - // Note these values could be different for each line in the file, but we only look at the first one - // - // Note that freqman requires a very specific string for these parameters, - // so check syntax in frequency file if specified value isn't being loaded - // - if (def_mod_index == -1) - def_mod_index = entry.modulation; + if (load_freqman_file(file_name, database, {})) { + loaded_file_name = file_name; + for (auto& entry_ptr : database) { + if (frequency_list.size() >= FREQMAN_MAX_PER_FILE) + break; - if (def_bw_index == -1) - def_bw_index = entry.bandwidth; + auto& entry = *entry_ptr; - if (def_step_index == -1) - def_step_index = entry.step; + // Get modulation & bw & step from file if specified + // Note these values could be different for each line in the file, but we only look at the first one + // + // Note that freqman requires a very specific string for these parameters, + // so check syntax in frequency file if specified value isn't being loaded + // + if (is_invalid(def_mod_index)) + def_mod_index = entry.modulation; - // Get frequency - if (entry.type == RANGE) { - if (!found_range) { - // Set Start & End Search Range instead of populating the small in-memory frequency table - // NOTE: There may be multiple single frequencies in file, but only one search range is supported. - found_range = true; - frequency_range.min = entry.frequency_a; - button_manual_start.set_text(to_string_short_freq(frequency_range.min)); - frequency_range.max = entry.frequency_b; - button_manual_end.set_text(to_string_short_freq(frequency_range.max)); - } - } else if (entry.type == SINGLE) { - found_single = true; - frequency_list.push_back(entry.frequency_a); - description_list.push_back(entry.description); - } else if (entry.type == HAMRADIO) { - // For HAM repeaters, add both receive & transmit frequencies to scan list and modify description - // (FUTURE fw versions might handle these differently) - found_single = true; - frequency_list.push_back(entry.frequency_a); - description_list.push_back("R:" + entry.description); + if (is_invalid(def_bw_index)) + def_bw_index = entry.bandwidth; - if ((entry.frequency_a != entry.frequency_b) && - (frequency_list.size() < FREQMAN_MAX_PER_FILE)) { - frequency_list.push_back(entry.frequency_b); - description_list.push_back("T:" + entry.description); - } + if (is_invalid(def_step_index)) + def_step_index = entry.step; + + // Get frequency + if (entry.type == freqman_type::Range) { + if (!found_range) { + // Set Start & End Search Range instead of populating the small in-memory frequency table + // NOTE: There may be multiple single frequencies in file, but only one search range is supported. + found_range = true; + frequency_range.min = entry.frequency_a; + button_manual_start.set_text(to_string_short_freq(frequency_range.min)); + frequency_range.max = entry.frequency_b; + button_manual_end.set_text(to_string_short_freq(frequency_range.max)); + } + } else if (entry.type == freqman_type::Single) { + found_single = true; + frequency_list.push_back(entry.frequency_a); + description_list.push_back(entry.description); + } else if (entry.type == freqman_type::HamRadio) { + // For HAM repeaters, add both receive & transmit frequencies to scan list and modify description + // (FUTURE fw versions might handle these differently) + found_single = true; + frequency_list.push_back(entry.frequency_a); + description_list.push_back("R:" + entry.description); + + if ((entry.frequency_a != entry.frequency_b) && + (frequency_list.size() < FREQMAN_MAX_PER_FILE)) { + frequency_list.push_back(entry.frequency_b); + description_list.push_back("T:" + entry.description); } - } else { - break; // No more space: Stop reading the txt file ! } } } else { @@ -650,13 +650,13 @@ void ScannerView::frequency_file_load(std::string file_name, bool stop_all_befor desc_freq_list_scan = desc_freq_list_scan + "..."; } - if ((def_mod_index != -1) && (def_mod_index != (freqman_index_t)field_mode.selected_index_value())) + if (is_valid(def_mod_index) && def_mod_index != (freqman_index_t)field_mode.selected_index_value()) field_mode.set_by_value(def_mod_index); // Update mode (also triggers a change callback that disables & reenables RF background) - if (def_bw_index != -1) // Update BW if specified in file + if (is_valid(def_bw_index)) // Update BW if specified in file field_bw.set_selected_index(def_bw_index); - if (def_step_index != -1) // Update step if specified in file + if (is_valid(def_step_index)) // Update step if specified in file field_step.set_selected_index(def_step_index); audio::output::stop(); diff --git a/firmware/application/apps/ui_scanner.hpp b/firmware/application/apps/ui_scanner.hpp index 0dfda179..21ddf989 100644 --- a/firmware/application/apps/ui_scanner.hpp +++ b/firmware/application/apps/ui_scanner.hpp @@ -90,7 +90,7 @@ class ScannerView : public View { std::string title() const override { return "Scanner"; }; std::vector frequency_list{}; - std::vector description_list{}; + std::vector description_list{}; // void set_parent_rect(const Rect new_parent_rect) override; diff --git a/firmware/application/file_reader.hpp b/firmware/application/file_reader.hpp index bc0f0507..bcd9f151 100644 --- a/firmware/application/file_reader.hpp +++ b/firmware/application/file_reader.hpp @@ -36,7 +36,8 @@ */ /* Iterates lines in buffer split on '\n'. - * NB: very basic iterator impl, don't try anything fancy with it. */ + * NB: very basic iterator impl, don't try anything fancy with it. + * For example, you _must_ deref the iterator after advancing it. */ template class BufferLineReader { public: @@ -133,4 +134,18 @@ using FileLineReader = BufferLineReader; * are used or they will dangle. */ std::vector split_string(std::string_view str, char c); +/* Returns the number of lines in a file. */ +template +uint32_t count_lines(BufferLineReader& reader) { + uint32_t count = 0; + auto it = reader.begin(); + + do { + *it; // Necessary to force the file read. + ++count; + } while (++it != reader.end()); + + return count; +} + #endif diff --git a/firmware/application/freqman.cpp b/firmware/application/freqman.cpp index 79fe7bda..21b1da7c 100644 --- a/firmware/application/freqman.cpp +++ b/firmware/application/freqman.cpp @@ -21,293 +21,66 @@ * Boston, MA 02110-1301, USA. */ +#include "file.hpp" #include "freqman.hpp" +#include "tone_key.hpp" +#include "ui_widget.hpp" + #include +namespace fs = std::filesystem; +using namespace tonekey; +using namespace ui; + using option_t = std::pair; using options_t = std::vector; -// TODO: Consolidate with receiver_model. -// These definitions are spread all over and stiched together with indices. -options_t freqman_entry_modulations = { - {"AM", 0}, - {"NFM", 1}, - {"WFM", 2}, - {"SPEC", 3}}; +extern options_t freqman_modulations; +extern options_t freqman_bandwidths[4]; +extern options_t freqman_steps; +extern options_t freqman_steps_short; -options_t freqman_entry_bandwidths[4] = { - {// AM - {"DSB 9k", 0}, - {"DSB 6k", 1}, - {"USB+3k", 2}, - {"LSB-3k", 3}, - {"CW", 4}}, - {// NFM - {"8k5", 0}, - {"11k", 1}, - {"16k", 2}}, - { - // WFM - {"40k", 2}, - {"180k", 1}, - {"200k", 0}, - }, - { - // SPEC - {"8k5", 8500}, - {"11k", 11000}, - {"16k", 16000}, - {"25k", 25000}, - {"50k", 50000}, - {"100k", 100000}, - {"250k", 250000}, - {"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/ - {"600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */ - {"650k", 650000}, - {"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */ - {"1100k", 1100000}, - {"1750k", 1750000}, - {"2000k", 2000000}, - {"2500k", 2500000}, - {"2750k", 2750000}, // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC) - }}; +const option_t* find_by_index(const options_t& options, freqman_index_t index) { + if (index < options.size()) + return &options[index]; + else + return nullptr; +} -options_t freqman_entry_steps = { - {"0.1kHz ", 100}, - {"1kHz ", 1000}, - {"5kHz (SA AM)", 5000}, - {"6.25kHz(NFM)", 6250}, - {"8.33kHz(AIR)", 8330}, - {"9kHz (EU AM)", 9000}, - {"10kHz(US AM)", 10000}, - {"12.5kHz(NFM)", 12500}, - {"15kHz (HFM)", 15000}, - {"25kHz (N1)", 25000}, - {"30kHz (OIRT)", 30000}, - {"50kHz (FM1)", 50000}, - {"100kHz (FM2)", 100000}, - {"250kHz (N2)", 250000}, - {"500kHz (WFM)", 500000}, - {"1MHz ", 1000000}}; - -options_t freqman_entry_steps_short = { - {"0.1kHz", 100}, - {"1kHz", 1000}, - {"5kHz", 5000}, - {"6.25kHz", 6250}, - {"8.33kHz", 8330}, - {"9kHz", 9000}, - {"10kHz", 10000}, - {"12.5kHz", 12500}, - {"15kHz", 15000}, - {"25kHz", 25000}, - {"30kHz", 30000}, - {"50kHz", 50000}, - {"100kHz", 100000}, - {"250kHz", 250000}, - {"500kHz", 500000}, - {"1MHz", 1000000}}; - -bool load_freqman_file(std::string& file_stem, freqman_db& db, bool load_freqs, bool load_ranges, bool load_hamradios, uint8_t max_num_freqs) { - // swap with empty vector to ensure memory is immediately released - std::vector().swap(db); - File freqman_file{}; - size_t length = 0, n = 0, file_position = 0; - char* pos = NULL; - char* line_start = NULL; - char* line_end = NULL; - std::string description{NULL}; - rf::Frequency frequency_a = 0, frequency_b = 0; - char file_data[FREQMAN_READ_BUF_SIZE + 1] = {0}; - freqman_entry_type type = NOTYPE; - freqman_index_t modulation = -1; - freqman_index_t bandwidth = -1; - freqman_index_t step = -1; - freqman_index_t tone = -1; - uint32_t tone_freq; - char c; - - auto result = freqman_file.open("FREQMAN/" + file_stem + ".TXT"); - if (result.is_valid()) - return false; - - while (1) { - // Read a FREQMAN_READ_BUF_SIZE block from file - freqman_file.seek(file_position); - - memset(file_data, 0, FREQMAN_READ_BUF_SIZE + 1); - auto read_size = freqman_file.read(file_data, FREQMAN_READ_BUF_SIZE); - if (read_size.is_error()) - return false; // Read error - - file_position += FREQMAN_READ_BUF_SIZE; - - // Reset line_start to beginning of buffer - line_start = file_data; - - // If EOF reached, insert 0x0A after, in case the last line doesn't have a C/R - if (read_size.value() < FREQMAN_READ_BUF_SIZE) - *(line_start + read_size.value()) = 0x0A; - - // Look for complete lines in buffer - while ((line_end = strstr(line_start, "\x0A"))) { - *line_end = 0; // Stop strstr() searches below at EOL - modulation = -1; - bandwidth = -1; - step = -1; - tone = -1; - type = NOTYPE; - - frequency_a = frequency_b = 0; - // Read frequency - pos = strstr(line_start, "f="); - if (pos) { - pos += 2; - frequency_a = strtoll(pos, nullptr, 10); - type = SINGLE; - } else { - // ...or range - pos = strstr(line_start, "a="); - if (pos) { - pos += 2; - frequency_a = strtoll(pos, nullptr, 10); - type = RANGE; - pos = strstr(line_start, "b="); - if (pos) { - pos += 2; - frequency_b = strtoll(pos, nullptr, 10); - } else - frequency_b = 0; - } else { - // ... or hamradio - pos = strstr(line_start, "r="); - if (pos) { - pos += 2; - frequency_a = strtoll(pos, nullptr, 10); - type = HAMRADIO; - pos = strstr(line_start, "t="); - if (pos) { - pos += 2; - frequency_b = strtoll(pos, nullptr, 10); - } else - frequency_b = frequency_a; - } else - frequency_a = 0; - } - } - // modulation if any - pos = strstr(line_start, "m="); - if (pos) { - pos += 2; - modulation = freqman_entry_get_modulation_from_str(pos); - } - // bandwidth if any - pos = strstr(line_start, "bw="); - if (pos) { - pos += 3; - bandwidth = freqman_entry_get_bandwidth_from_str(modulation, pos); - } - // step if any - pos = strstr(line_start, "s="); - if (pos) { - pos += 2; - step = freqman_entry_get_step_from_str_short(pos); - } - // ctcss tone if any - pos = strstr(line_start, "c="); - if (pos) { - pos += 2; - // find decimal point and replace with 0 if there is one, for strtoll - length = strcspn(pos, ".,\x0A"); - if (pos + length <= line_end) { - c = *(pos + length); - *(pos + length) = 0; - // ASCII Hz to integer Hz x 100 - tone_freq = strtoll(pos, nullptr, 10) * 100; - // stuff saved character back into string in case it was not a decimal point - *(pos + length) = c; - // now get first digit after decimal point (10ths of Hz) - pos += length + 1; - if (c == '.' && *pos >= '0' && *pos <= '9') - tone_freq += (*pos - '0') * 10; - // convert tone_freq (100x the freq in Hz) to a tone_key index - tone = tone_key_index_by_value(tone_freq); - } - } - // Read description until , or LF - pos = strstr(line_start, "d="); - if (pos) { - pos += 2; - length = std::min(strcspn(pos, ",\x0A"), (size_t)FREQMAN_DESC_MAX_LEN); - description = string(pos, length); - description.shrink_to_fit(); - } - if ((type == SINGLE && load_freqs) || (type == RANGE && load_ranges) || (type == HAMRADIO && load_hamradios)) { - freqman_entry entry = {frequency_a, frequency_b, std::move(description), type, modulation, bandwidth, step, tone}; - db.emplace_back(entry); - n++; - if (n > max_num_freqs) return true; - } - - line_start = line_end + 1; - if (line_start - file_data >= FREQMAN_READ_BUF_SIZE) break; - } - - if (read_size.value() != FREQMAN_READ_BUF_SIZE) - break; // End of file - - // Restart at beginning of last incomplete line - file_position -= (file_data + FREQMAN_READ_BUF_SIZE - line_start); - } - - /* populate implicitly specified modulation / bandwidth */ - if (db.size() > 2) { - modulation = db[0].modulation; - bandwidth = db[0].bandwidth; - - for (unsigned int it = 1; it < db.size(); it++) { - if (db[it].modulation < 0) { - db[it].modulation = modulation; - } else { - modulation = db[it].modulation; - } - if (db[it].bandwidth < 0) { - db[it].bandwidth = bandwidth; - } else { - modulation = db[it].bandwidth; - } - } - } - db.shrink_to_fit(); - return true; +// TODO: move into FreqmanDB type +/* Freqman file handling. */ +bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) { + fs::path path{u"FREQMAN/"}; + path += file_stem + ".TXT"; + return parse_freqman_file(path, db, options); } bool get_freq_string(freqman_entry& entry, std::string& item_string) { rf::Frequency frequency_a, frequency_b; frequency_a = entry.frequency_a; - if (entry.type == SINGLE) { + if (entry.type == freqman_type::Single) { // Single item_string = "f=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0'); - } else if (entry.type == RANGE) { + } else if (entry.type == freqman_type::Range) { // Range frequency_b = entry.frequency_b; item_string = "a=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0'); item_string += ",b=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0'); - if (entry.step >= 0) { + if (is_valid(entry.step)) { item_string += ",s=" + freqman_entry_get_step_string_short(entry.step); } - } else if (entry.type == HAMRADIO) { + } else if (entry.type == freqman_type::HamRadio) { frequency_b = entry.frequency_b; item_string = "r=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0'); item_string += ",t=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0'); - if (entry.tone >= 0) { + if (is_valid(entry.tone)) { item_string += ",c=" + tone_key_value_string(entry.tone); } } - if (entry.modulation >= 0 && (unsigned)entry.modulation < freqman_entry_modulations.size()) { + if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) { item_string += ",m=" + freqman_entry_get_modulation_string(entry.modulation); - if (entry.bandwidth >= 0 && (unsigned)entry.bandwidth < freqman_entry_bandwidths[entry.modulation].size()) { + if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size()) { item_string += ",bw=" + freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth); } } @@ -317,14 +90,14 @@ bool get_freq_string(freqman_entry& entry, std::string& item_string) { return true; } -bool delete_freqman_file(std::string& file_stem) { +bool delete_freqman_file(const std::string& file_stem) { File freqman_file; std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT"; delete_file(freq_file_path); return false; } -bool save_freqman_file(std::string& file_stem, freqman_db& db) { +bool save_freqman_file(const std::string& file_stem, freqman_db& db) { File freqman_file; std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT"; delete_file(freq_file_path); @@ -332,7 +105,7 @@ bool save_freqman_file(std::string& file_stem, freqman_db& db) { if (!result.is_valid()) { for (size_t n = 0; n < db.size(); n++) { std::string line; - get_freq_string(db[n], line); + get_freq_string(*db[n], line); freqman_file.write_line(line); } return true; @@ -340,7 +113,7 @@ bool save_freqman_file(std::string& file_stem, freqman_db& db) { return false; } -bool create_freqman_file(std::string& file_stem, File& freqman_file) { +bool create_freqman_file(const std::string& file_stem, File& freqman_file) { auto result = freqman_file.create("FREQMAN/" + file_stem + ".TXT"); if (result.is_valid()) @@ -353,17 +126,17 @@ std::string freqman_item_string(freqman_entry& entry, size_t max_length) { std::string item_string; switch (entry.type) { - case SINGLE: + case freqman_type::Single: item_string = to_string_short_freq(entry.frequency_a) + "M: " + entry.description; break; - case RANGE: + case freqman_type::Range: item_string = "R: " + entry.description; break; - case HAMRADIO: + case freqman_type::HamRadio: item_string = "H: " + entry.description; break; default: - item_string = "!UNKNOW TYPE " + entry.description; + item_string = "!UNKNOWN TYPE " + entry.description; break; } @@ -373,115 +146,69 @@ std::string freqman_item_string(freqman_entry& entry, size_t max_length) { return item_string; } +/* Set options. */ void freqman_set_modulation_option(OptionsField& option) { - option.set_options(freqman_entry_modulations); + option.set_options(freqman_modulations); } void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) { - option.set_options(freqman_entry_bandwidths[modulation]); + if (is_valid(modulation)) + option.set_options(freqman_bandwidths[modulation]); } void freqman_set_step_option(OptionsField& option) { - option.set_options(freqman_entry_steps); + option.set_options(freqman_steps); } void freqman_set_step_option_short(OptionsField& option) { - option.set_options(freqman_entry_steps_short); + option.set_options(freqman_steps_short); } +/* Option name lookup. */ std::string freqman_entry_get_modulation_string(freqman_index_t modulation) { - if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) { - return std::string(""); // unknown modulation - } - return freqman_entry_modulations[modulation].first; + if (auto opt = find_by_index(freqman_modulations, modulation)) + return opt->first; + return {}; } std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) { - if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) { - return std::string(""); // unknown modulation + if (modulation < freqman_modulations.size()) { + if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth)) + return opt->first; } - if (bandwidth < 0 || (unsigned)bandwidth > freqman_entry_bandwidths[modulation].size()) { - return std::string(""); // unknown modulation - } - return freqman_entry_bandwidths[modulation][bandwidth].first; + return {}; } std::string freqman_entry_get_step_string(freqman_index_t step) { - if (step < 0 || (unsigned)step >= freqman_entry_steps.size()) { - return std::string(""); // unknown modulation - } - return freqman_entry_steps[step].first; + if (auto opt = find_by_index(freqman_steps, step)) + return opt->first; + return {}; } std::string freqman_entry_get_step_string_short(freqman_index_t step) { - if (step < 0 || (unsigned)step >= freqman_entry_steps_short.size()) { - return std::string(""); // unknown modulation - } - return freqman_entry_steps_short[step].first; + if (auto opt = find_by_index(freqman_steps_short, step)) + return opt->first; + return {}; } +/* Option value lookup. */ +// TODO: use Optional instead of magic values. int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) { - if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) { - return -1; // unknown modulation - } - return freqman_entry_modulations[modulation].second; + 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 < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) { - return -1; // unknown modulation + if (modulation < freqman_modulations.size()) { + if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth)) + return opt->second; } - if (bandwidth < 0 || (unsigned)bandwidth > freqman_entry_bandwidths[modulation].size()) { - return -1; // unknown bandwidth for modulation - } - return freqman_entry_bandwidths[modulation][bandwidth].second; + return -1; } int32_t freqman_entry_get_step_value(freqman_index_t step) { - if (step < 0 || (unsigned)step >= freqman_entry_steps.size()) { - return -1; // unknown modulation - } - return freqman_entry_steps[step].second; -} - -freqman_index_t freqman_entry_get_modulation_from_str(char* str) { - if (!str) - return -1; - for (freqman_index_t index = 0; (unsigned)index < freqman_entry_modulations.size(); index++) { - if (strncmp(freqman_entry_modulations[index].first.c_str(), str, freqman_entry_modulations[index].first.size()) == 0) - return index; - } - return -1; -} - -freqman_index_t freqman_entry_get_bandwidth_from_str(freqman_index_t modulation, char* str) { - if (!str) - return -1; - if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) - return -1; - for (freqman_index_t index = 0; (unsigned)index < freqman_entry_bandwidths[modulation].size(); index++) { - if (strncmp(freqman_entry_bandwidths[modulation][index].first.c_str(), str, freqman_entry_bandwidths[modulation][index].first.size()) == 0) - return index; - } - return -1; -} - -freqman_index_t freqman_entry_get_step_from_str(char* str) { - if (!str) - return -1; - for (freqman_index_t index = 0; (unsigned)index < freqman_entry_steps.size(); index++) { - if (strncmp(freqman_entry_steps[index].first.c_str(), str, freqman_entry_steps[index].first.size()) == 0) - return index; - } - return -1; -} - -freqman_index_t freqman_entry_get_step_from_str_short(char* str) { - if (!str) - return -1; - for (freqman_index_t index = 0; (unsigned)index < freqman_entry_steps_short.size(); index++) { - if (strncmp(freqman_entry_steps_short[index].first.c_str(), str, freqman_entry_steps_short[index].first.size()) == 0) - return index; - } + if (auto opt = find_by_index(freqman_steps, step)) + return opt->second; return -1; } diff --git a/firmware/application/freqman.hpp b/firmware/application/freqman.hpp index 658664dc..ad55cd9d 100644 --- a/firmware/application/freqman.hpp +++ b/firmware/application/freqman.hpp @@ -27,23 +27,13 @@ #include #include #include "file.hpp" -#include "ui_receiver.hpp" -#include "tone_key.hpp" +#include "freqman_db.hpp" #include "string_format.hpp" +#include "ui_receiver.hpp" #include "ui_widget.hpp" -#define FREQMAN_DESC_MAX_LEN 24 // This is the number of characters that can be drawn in front of "R: TEXT..." before taking a full screen line -#define FREQMAN_MAX_PER_FILE 90 // Maximum of entries we can read. This is a hardware limit - // It was tested and lowered to leave a bit of space to the caller - -#define FREQMAN_READ_BUF_SIZE 96 // max freqman line size including desc is 90, + a bit of space - -using namespace ui; -using namespace std; -using namespace tonekey; - -// needs to be signed as -1 means not set -typedef int8_t freqman_index_t; +// Defined for back-compat. +#define FREQMAN_MAX_PER_FILE freqman_default_max_entries enum freqman_error : int8_t { NO_ERROR = 0, @@ -52,59 +42,26 @@ enum freqman_error : int8_t { ERROR_DUPLICATE }; -enum freqman_entry_type : int8_t { - SINGLE = 0, // f= - RANGE, // a=,b= - HAMRADIO, // r=,t= - NOTYPE // undetected -}; - enum freqman_entry_modulation : uint8_t { AM_MODULATION = 0, NFM_MODULATION, WFM_MODULATION, - SPEC_MODULATION, + SPEC_MODULATION }; -// Entry step placed for AlainD freqman version (or any other enhanced version) -enum freqman_entry_step : int8_t { - AM_US, // 10 kHz AM/CB - AM_EUR, // 9 kHz LW/MW - NFM_1, // 12,5 kHz (Analogic PMR 446) - NFM_2, // 6,25 kHz (Digital PMR 446) - FM_1, // 100 kHz - FM_2, // 50 kHz - N_1, // 25 kHz - N_2, // 250 kHz - AIRBAND, // AIRBAND 8,33 kHz -}; - -struct freqman_entry { - rf::Frequency frequency_a{0}; // 'f=freq' or 'a=freq_start' or 'r=recv_freq' - rf::Frequency frequency_b{0}; // 'b=freq_end' or 't=tx_freq' - std::string description{NULL}; // 'd=desc' - freqman_entry_type type{SINGLE}; // SINGLE,RANGE,HAMRADIO - freqman_index_t modulation{AM_MODULATION}; // AM,NFM,WFM - freqman_index_t bandwidth{0}; // AM_DSB, ... - freqman_index_t step{0}; // 5khz (SA AM,... - tone_index tone{0}; // 0XZ, 11 1ZB,... -}; - -using freqman_db = std::vector; - -bool load_freqman_file(std::string& file_stem, freqman_db& db, bool load_freqs = true, bool load_ranges = true, bool load_hamradios = true, uint8_t max_num_freqs = FREQMAN_MAX_PER_FILE); +bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options); bool get_freq_string(freqman_entry& entry, std::string& item_string); -bool delete_freqman_file(std::string& file_stem); -bool save_freqman_file(std::string& file_stem, freqman_db& db); -bool create_freqman_file(std::string& file_stem, File& freqman_file); +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, File& freqman_file); std::string freqman_item_string(freqman_entry& item, size_t max_length); -void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option); -void freqman_set_modulation_option(OptionsField& option); -void freqman_set_step_option(OptionsField& option); -void freqman_set_step_option_short(OptionsField& option); -void freqman_set_tone_option(OptionsField& option); +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); 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); @@ -115,9 +72,4 @@ 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); -freqman_index_t freqman_entry_get_modulation_from_str(char* str); -freqman_index_t freqman_entry_get_bandwidth_from_str(freqman_index_t modulation, char* str); -freqman_index_t freqman_entry_get_step_from_str(char* str); -freqman_index_t freqman_entry_get_step_from_str_short(char* str); - #endif /*__FREQMAN_H__*/ diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp new file mode 100644 index 00000000..b1c75547 --- /dev/null +++ b/firmware/application/freqman_db.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * Copyright (C) 2023 gullradriel, Nilorea Studio Inc. + * 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. + */ + +#include "convert.hpp" +#include "file.hpp" +#include "file_reader.hpp" +#include "freqman_db.hpp" +#include "string_format.hpp" +#include "tone_key.hpp" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +using option_t = std::pair; +using options_t = std::vector; + +options_t freqman_modulations = { + {"AM", 0}, + {"NFM", 1}, + {"WFM", 2}, + {"SPEC", 3}, +}; + +options_t freqman_bandwidths[4] = { + { + // AM + {"DSB 9k", 0}, + {"DSB 6k", 1}, + {"USB+3k", 2}, + {"LSB-3k", 3}, + {"CW", 4}, + }, + { + // NFM + {"8k5", 0}, + {"11k", 1}, + {"16k", 2}, + }, + { + // WFM + {"40k", 2}, + {"180k", 1}, + {"200k", 0}, + }, + { + // SPEC -- TODO: these should be indexes. + {"8k5", 8500}, + {"11k", 11000}, + {"16k", 16000}, + {"25k", 25000}, + {"50k", 50000}, + {"100k", 100000}, + {"250k", 250000}, + {"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/ + {"600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */ + {"650k", 650000}, + {"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */ + {"1100k", 1100000}, + {"1750k", 1750000}, + {"2000k", 2000000}, + {"2500k", 2500000}, + {"2750k", 2750000}, // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC) + }, +}; + +options_t freqman_steps = { + {"0.1kHz ", 100}, + {"1kHz ", 1000}, + {"5kHz (SA AM)", 5000}, + {"6.25kHz(NFM)", 6250}, + {"8.33kHz(AIR)", 8330}, + {"9kHz (EU AM)", 9000}, + {"10kHz(US AM)", 10000}, + {"12.5kHz(NFM)", 12500}, + {"15kHz (HFM)", 15000}, + {"25kHz (N1)", 25000}, + {"30kHz (OIRT)", 30000}, + {"50kHz (FM1)", 50000}, + {"100kHz (FM2)", 100000}, + {"250kHz (N2)", 250000}, + {"500kHz (WFM)", 500000}, + {"1MHz ", 1000000}, +}; + +options_t freqman_steps_short = { + {"0.1kHz", 100}, + {"1kHz", 1000}, + {"5kHz", 5000}, + {"6.25kHz", 6250}, + {"8.33kHz", 8330}, + {"9kHz", 9000}, + {"10kHz", 10000}, + {"12.5kHz", 12500}, + {"15kHz", 15000}, + {"25kHz", 25000}, + {"30kHz", 30000}, + {"50kHz", 50000}, + {"100kHz", 100000}, + {"250kHz", 250000}, + {"500kHz", 500000}, + {"1MHz", 1000000}, +}; + +uint8_t find_by_name(const options_t& options, std::string_view name) { + for (auto ix = 0u; ix < options.size(); ++ix) + if (options[ix].first == name) + return ix; + + return freqman_invalid_index; +} + +/* Impl for next round of changes. + *template + *const T* find_by_name(const std::array& info, std::string_view name) { + * for (const auto& it : info) { + * if (it.name == name) + * return ⁢ + * } + * + * return nullptr; + *} + */ + +// TODO: How much format validation should this do? +// It's very permissive right now, but entries can be invalid. +// TODO: parse_int seems to hang on invalid input. +bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { + if (str.empty() || str[0] == '#') + return false; + + auto cols = split_string(str, ','); + entry = freqman_entry{}; + + for (auto col : cols) { + if (col.empty()) + continue; + + auto pair = split_string(col, '='); + if (pair.size() != 2) + continue; + + auto key = pair[0]; + auto value = pair[1]; + + if (key == "a") { + entry.type = freqman_type::Range; + parse_int(value, entry.frequency_a); + } else if (key == "b") { + parse_int(value, entry.frequency_b); + } else if (key == "bw") { + // NB: Requires modulation to be set first + if (entry.modulation < std::size(freqman_bandwidths)) { + entry.bandwidth = find_by_name(freqman_bandwidths[entry.modulation], value); + } + } else if (key == "c") { + // Split into whole and fractional parts. + auto parts = split_string(value, '.'); + int32_t tone_freq = 0; + int32_t whole_part = 0; + parse_int(parts[0], whole_part); + + // Tones are stored as frequency / 100 for some reason. + // E.g. 14572 would be 145.7 (NB: 1s place is dropped). + // TODO: Might be easier to just store the codes? + // Multiply the whole part by 100 to get the tone frequency. + tone_freq = whole_part * 100; + + // Add the fractional part, if present. + if (parts.size() > 1) { + auto c = parts[1].front(); + auto digit = std::isdigit(c) ? c - '0' : 0; + tone_freq += digit * 10; + } + entry.tone = static_cast( + tonekey::tone_key_index_by_value(tone_freq)); + } else if (key == "d") { + entry.description = trim(value); + } else if (key == "f") { + entry.type = freqman_type::Single; + parse_int(value, entry.frequency_a); + } else if (key == "m") { + entry.modulation = find_by_name(freqman_modulations, value); + } else if (key == "r") { + entry.type = freqman_type::HamRadio; + parse_int(value, entry.frequency_a); + } else if (key == "s") { + entry.step = find_by_name(freqman_steps_short, value); + } else if (key == "t") { + parse_int(value, entry.frequency_b); + } + } + + return true; +} + +bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) { + File f; + auto error = f.open(path); + if (error) + 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; + + // Filter by entry type. + if ((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; + } + + // Use previous entry's mod/band if current's aren't set. + if (!db.empty()) { + if (is_invalid(entry.modulation)) + entry.modulation = db.back()->modulation; + if (is_invalid(entry.bandwidth)) + entry.bandwidth = db.back()->bandwidth; + } + + // Move the entry onto the heap and push. + db.push_back(std::make_unique(std::move(entry))); + + // Limit to max_entries when specified. + if (options.max_entries > 0 && db.size() >= options.max_entries) + break; + } + + db.shrink_to_fit(); + return true; +} \ No newline at end of file diff --git a/firmware/application/freqman_db.hpp b/firmware/application/freqman_db.hpp new file mode 100644 index 00000000..83c1d39b --- /dev/null +++ b/firmware/application/freqman_db.hpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * Copyright (C) 2023 gullradriel, Nilorea Studio Inc., 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 __FREQMAN_DB_H__ +#define __FREQMAN_DB_H__ + +#include "file.hpp" +#include "utility.hpp" + +#include +#include +#include +#include +#include + +using freqman_index_t = uint8_t; +constexpr freqman_index_t freqman_invalid_index = static_cast(-1); + +/* Returns true if the value is not invalid_index. */ +constexpr bool is_valid(freqman_index_t index) { + return index != freqman_invalid_index; +} + +/* Returns true if the value is invalid_index. */ +constexpr bool is_invalid(freqman_index_t index) { + return index == freqman_invalid_index; +} + +enum class freqman_type : uint8_t { + Single, // f= + Range, // a=,b= + HamRadio, // r=,t= + Unknown, +}; + +/* Tables for next round of changes. + * // TODO: attempt to consolidate all of these values + * // and settings into a single location. + * // Should tone_keys get consolidated here too? + * enum class freqman_modulation : uint8_t { + AM, + NFM, + WFM, + SPEC, + Unknown, + * }; + * + * struct freqman_modulation_info { + * freqman_modulation modulation; + * std::string_view name; + * }; + * + * constexpr std::array freqman_modulations = { + freqman_modulation_info{ freqman_modulation::AM, "AM" }, + freqman_modulation_info{ freqman_modulation::NFM, "NFM" }, + freqman_modulation_info{ freqman_modulation::WFM, "WFM" }, + freqman_modulation_info{ freqman_modulation::SPEC, "SPEC" }, + freqman_modulation_info{ freqman_modulation::Unknown, "Unknown" } + * }; + * static_assert(std::size(freqman_modulations) == (size_t)freqman_modulation::Unknown + 1); + * + * enum class freqman_step : uint8_t { + _100Hz, + _1kHz, + _5kHz, + _6_25kHz, + _8_33kHz, + _9kHz, + _10kHz, + _12_5kHz, + _15kHz, + _25kHz, + _30kHz, + _50kHz, + _100kHz, + _250kHz, + _500kHz, + _1MHz, + Unknown, + * }; + * + * struct freqman_step_info { + * freqman_step step; + * std::string_view name; + * std::string_view display_name; + * uint32_t value; + * }; + * + * // TODO: FrequencyStepView should use this list. + * constexpr std::array freqman_steps = { + freqman_step_info{ freqman_step::_100Hz, "0.1kHz", "0.1kHz ", 100 }, + freqman_step_info{ freqman_step::_1kHz, "1kHz", "1kHz ", 1'000 }, + freqman_step_info{ freqman_step::_5kHz, "5kHz", "5kHz (SA AM)", 5'000 }, + freqman_step_info{ freqman_step::_6_25kHz, "6.25kHz", "6.25kHz(NFM)", 6'250 }, + freqman_step_info{ freqman_step::_8_33kHz, "8.33kHz", "8.33kHz(AIR)", 8'330 }, + freqman_step_info{ freqman_step::_9kHz, "9kHz", "9kHz (EU AM)", 9'000 }, + freqman_step_info{ freqman_step::_10kHz, "10kHz", "10kHz(US AM)", 10'000 }, + freqman_step_info{ freqman_step::_12_5kHz, "12.5kHz", "12.5kHz(NFM)", 12'500 }, + freqman_step_info{ freqman_step::_15kHz, "15kHz", "15kHz (HFM)", 15'000 }, + freqman_step_info{ freqman_step::_25kHz, "25kHz", "25kHz (N1)", 25'000 }, + freqman_step_info{ freqman_step::_30kHz, "30kHz", "30kHz (OIRT)", 30'000 }, + freqman_step_info{ freqman_step::_50kHz, "50kHz", "50kHz (FM1)", 50'000 }, + freqman_step_info{ freqman_step::_100kHz, "100kHz", "100kHz (FM2)", 100'000 }, + freqman_step_info{ freqman_step::_250kHz, "250kHz", "250kHz (N2)", 250'000 }, + freqman_step_info{ freqman_step::_500kHz, "500kHz", "500kHz (WFM)", 500'000 }, + freqman_step_info{ freqman_step::_1MHz, "1MHz", "1MHz ", 1'000'000 }, + freqman_step_info{ freqman_step::Unknown, "Unknown", "Unknown ", 0 }, + * }; + * static_assert(std::size(freqman_steps) == (size_t)freqman_step::Unknown + 1); +*/ + +/* Freqman Entry *******************************/ +struct freqman_entry { + int64_t frequency_a{0}; // 'f=freq' or 'a=freq_start' or 'r=recv_freq' + int64_t frequency_b{0}; // 'b=freq_end' or 't=tx_freq' + std::string description{0}; // 'd=desc' + freqman_type type{freqman_type::Unknown}; + freqman_index_t modulation{freqman_invalid_index}; + freqman_index_t bandwidth{freqman_invalid_index}; + freqman_index_t step{freqman_invalid_index}; + freqman_index_t tone{freqman_invalid_index}; +}; + +/* A reasonable maximum number of items to load from a freqman file. + * Apps using freqman_db should be tested and this value tuned to + * ensure app memory stability. */ +constexpr size_t freqman_default_max_entries = 90; + +struct freqman_load_options { + /* Loads all entries when set to 0. */ + size_t max_entries{freqman_default_max_entries}; + bool load_freqs{true}; + bool load_ranges{true}; + bool load_hamradios{true}; +}; + +using freqman_entry_ptr = std::unique_ptr; +using freqman_db = std::vector; + +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); + +/* Type for next round of changes. + *class FreqmanDB { + * public: + * FreqmanDB(); + * FreqmanDB(const FreqmanDB&) = delete; + * FreqmanDB(FreqmanDB&&) = delete; + * FreqmanDB& operator=(const FreqmanDB&) = delete; + * FreqmanDB& operator=(FreqmanDB&&) = delete; + * + * size_t size() const { return 0; }; + * + * private: + * freqman_db entries_; + *}; + */ + +#endif /* __FREQMAN_DB_H__ */ \ No newline at end of file diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index c6e999c3..f7ad5fbe 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -177,7 +177,7 @@ static void to_string_hex_internal(char* p, const uint64_t n, const int32_t l) { std::string to_string_hex(const uint64_t n, int32_t l) { char p[32]; - l = std::min(l, 31L); + l = std::min(l, 31); to_string_hex_internal(p, n, l - 1); p[l] = 0; return p; diff --git a/firmware/application/tone_key.cpp b/firmware/application/tone_key.cpp index d2402426..5f6aaf98 100644 --- a/firmware/application/tone_key.cpp +++ b/firmware/application/tone_key.cpp @@ -88,25 +88,6 @@ std::string fx100_string(uint32_t f) { return to_string_dec_uint(f / 100) + "." + to_string_dec_uint((f / 10) % 10); } -void tone_keys_populate(OptionsField& field) { - using option_t = std::pair; - using options_t = std::vector; - options_t tone_key_options; - std::string tone_name; - - for (size_t c = 0; c < tone_keys.size(); c++) { - auto f = tone_keys[c].second; - if ((c != 0) && (f < 1000 * 100)) - tone_name = "CTCSS " + fx100_string(f) + " #" + tone_keys[c].first; - else - tone_name = tone_keys[c].first; - - tone_key_options.emplace_back(tone_name, c); - } - - field.set_options(tone_key_options); -} - float tone_key_frequency(tone_index index) { return float(tone_keys[index].second) / 100.0; } @@ -169,7 +150,7 @@ std::string tone_key_string_by_value(uint32_t value, size_t max_length) { // Value is in 0.01 Hz units tone_index tone_key_index_by_value(uint32_t value) { uint32_t diff; - uint32_t min_diff{value * 2}; + uint32_t min_diff{UINT32_MAX}; tone_index min_idx{-1}; tone_index idx; @@ -196,7 +177,7 @@ tone_index tone_key_index_by_string(char* str) { if (!str) return -1; for (tone_index index = 0; (unsigned)index < tone_keys.size(); index++) { - if (tone_keys[index].first.compare(str) >= 0) + if (tone_keys[index].first.compare(str) >= 0) // TODO: why >=? return index; } return -1; diff --git a/firmware/application/tone_key.hpp b/firmware/application/tone_key.hpp index 6fc24927..f2e998f0 100644 --- a/firmware/application/tone_key.hpp +++ b/firmware/application/tone_key.hpp @@ -23,10 +23,9 @@ #ifndef __TONE_KEY_H_ #define __TONE_KEY_H_ -#include "ui.hpp" -#include "ui_widget.hpp" - -using namespace ui; +#include +#include +#include namespace tonekey { @@ -34,15 +33,14 @@ namespace tonekey { #define TONE_DISPLAY_TOGGLE_COUNTER 3 #define F2Ix100(x) (int32_t)(x * 100.0) -typedef int32_t tone_index; - +using tone_index = int32_t; using tone_key_t = std::vector>; extern const tone_key_t tone_keys; -void tone_keys_populate(OptionsField& field); float tone_key_frequency(tone_index index); +std::string fx100_string(uint32_t f); std::string tone_key_string(tone_index index); std::string tone_key_value_string(tone_index index); std::string tone_key_string_by_value(uint32_t value, size_t max_length); diff --git a/firmware/application/ui/ui_freqlist.cpp b/firmware/application/ui/ui_freqlist.cpp index 4423b048..7286bf3a 100644 --- a/firmware/application/ui/ui_freqlist.cpp +++ b/firmware/application/ui/ui_freqlist.cpp @@ -76,7 +76,7 @@ void FreqManUIList::paint(Painter& painter) { for (uint8_t it = current_index; it < freqlist_db->size(); it++) { uint8_t line_height = (int)nb_lines * char_height; if (line_height < (r.height() - char_height)) { // line is within the widget - std::string description = freqman_item_string(freqlist_db->at(it), 30); + std::string description = freqman_item_string(*freqlist_db->at(it), 30); if (nb_lines == highlighted_index) { const Rect r_highlighted_freq{0, r.location().y() + (int)nb_lines * char_height, 240, char_height}; painter.fill_rectangle( diff --git a/firmware/application/ui/ui_receiver.cpp b/firmware/application/ui/ui_receiver.cpp index cc53409b..c015e807 100644 --- a/firmware/application/ui/ui_receiver.cpp +++ b/firmware/application/ui/ui_receiver.cpp @@ -96,7 +96,7 @@ bool FrequencyField::on_encoder(const EncoderEvent delta) { // To get these magic numbers, I graphed the function until the // curve shape seemed about right then tested on device. delta_ms = std::min(145ull, delta_ms) + 5; // Prevent DIV/0 - int64_t scale = 200'000'000 / (0.001'55 * pow(delta_ms, 5.45)) + 8; + int64_t scale = 200'000'000 / (0.001'55 * std::pow(delta_ms, 5.45)) + 8; set_value(value() + (delta * scale)); } else { set_value(value() + (delta * step)); diff --git a/firmware/application/ui/ui_tone_key.cpp b/firmware/application/ui/ui_tone_key.cpp new file mode 100644 index 00000000..019be359 --- /dev/null +++ b/firmware/application/ui/ui_tone_key.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * + * 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. + */ + +#include "ui_tone_key.hpp" +#include + +using namespace ui; + +namespace tonekey { + +void tone_keys_populate(OptionsField& field) { + OptionsField::options_t tone_key_options; + std::string tone_name; + + for (size_t c = 0; c < tone_keys.size(); c++) { + auto f = tone_keys[c].second; + if ((c != 0) && (f < 1000 * 100)) + tone_name = "CTCSS " + fx100_string(f) + " #" + tone_keys[c].first; + else + tone_name = tone_keys[c].first; + + tone_key_options.emplace_back(tone_name, c); + } + + field.set_options(std::move(tone_key_options)); +} + +} // namespace tonekey diff --git a/firmware/application/ui/ui_tone_key.hpp b/firmware/application/ui/ui_tone_key.hpp new file mode 100644 index 00000000..c838b31a --- /dev/null +++ b/firmware/application/ui/ui_tone_key.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * + * 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 __UI_TONE_KEY_H_ +#define __UI_TONE_KEY_H_ + +#include "tone_key.hpp" +#include "ui.hpp" +#include "ui_widget.hpp" + +namespace tonekey { + +void tone_keys_populate(ui::OptionsField& field); + +} // namespace tonekey + +#endif /*__UI_TONE_KEY_H_*/ diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index a5ce1783..d2c11b3c 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1390,12 +1390,12 @@ void ImageToggle::set_value(bool b) { /* ImageOptionsField *****************************************************/ ImageOptionsField::ImageOptionsField( - const Rect parent_rect, - const Color foreground, - const Color background, - const options_t options) + Rect parent_rect, + Color foreground, + Color background, + options_t options) : Widget{parent_rect}, - options{options}, + options{std::move(options)}, foreground_{foreground}, background_{background} { set_focusable(true); @@ -1437,7 +1437,7 @@ void ImageOptionsField::set_by_value(value_t v) { } void ImageOptionsField::set_options(options_t new_options) { - options = new_options; + options = std::move(new_options); // Set an invalid index to force on_change. selected_index_ = (size_t)-1; @@ -1486,7 +1486,7 @@ OptionsField::OptionsField( options_t options) : Widget{{parent_pos, {8 * length, 16}}}, length_{length}, - options{options} { + options{std::move(options)} { set_focusable(true); } @@ -1543,7 +1543,7 @@ void OptionsField::set_by_nearest_value(value_t v) { } void OptionsField::set_options(options_t new_options) { - options = new_options; + options = std::move(new_options); // Set an invalid index to force on_change. selected_index_ = (size_t)-1; diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 994a3223..77d26a78 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -575,10 +575,10 @@ class ImageOptionsField : public Widget { std::function on_show_options{}; ImageOptionsField( - const Rect parent_rect, - const Color foreground, - const Color background, - const options_t options); + Rect parent_rect, + Color foreground, + Color background, + options_t options); ImageOptionsField() : ImageOptionsField{{}, Color::white(), Color::black(), {}} { diff --git a/firmware/test/application/CMakeLists.txt b/firmware/test/application/CMakeLists.txt index 36c4b828..0ce4213e 100644 --- a/firmware/test/application/CMakeLists.txt +++ b/firmware/test/application/CMakeLists.txt @@ -39,11 +39,19 @@ add_executable(application_test EXCLUDE_FROM_ALL ${PROJECT_SOURCE_DIR}/test_convert.cpp ${PROJECT_SOURCE_DIR}/test_file_reader.cpp ${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp + ${PROJECT_SOURCE_DIR}/test_freqman_db.cpp ${PROJECT_SOURCE_DIR}/test_mock_file.cpp ${PROJECT_SOURCE_DIR}/test_optional.cpp ${PROJECT_SOURCE_DIR}/test_utility.cpp ${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp + ${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp + ${PROJECT_SOURCE_DIR}/../../application/string_format.cpp + + # Dependencies + ${PROJECT_SOURCE_DIR}/../../application/file.cpp + ${PROJECT_SOURCE_DIR}/../../application/tone_key.cpp + ${PROJECT_SOURCE_DIR}/linker_stubs.cpp ) target_include_directories(application_test PRIVATE diff --git a/firmware/test/application/linker_stubs.cpp b/firmware/test/application/linker_stubs.cpp new file mode 100644 index 00000000..18915134 --- /dev/null +++ b/firmware/test/application/linker_stubs.cpp @@ -0,0 +1,80 @@ +/* + * 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. + */ + +/* This file contains stub functions necessary to enable linking. + * Try to minimize dependecies by breaking code into separate files + * or using templates and mock types. Because the test code is built + * and executed on the dev machine, a lot of core firmware code + * will not or cannot work (e.g. filesystem). We could build abstractions + * but that's just device overhead that only supports testing. */ + +#include + +/* FatFS stubs */ +#include "ff.h" +FRESULT f_close(FIL*) { + return FR_OK; +} +FRESULT f_closedir(DIR*) { + return FR_OK; +} +FRESULT f_findfirst(DIR*, FILINFO*, const TCHAR*, const TCHAR*) { + return FR_OK; +} +FRESULT f_findnext(DIR*, FILINFO*) { + return FR_OK; +} +FRESULT f_getfree(const TCHAR*, DWORD*, FATFS**) { + return FR_OK; +} +FRESULT f_lseek(FIL*, FSIZE_t) { + return FR_OK; +} +FRESULT f_mkdir(const TCHAR*) { + return FR_OK; +} +FRESULT f_open(FIL*, const TCHAR*, BYTE) { + return FR_OK; +} +FRESULT f_read(FIL*, void*, UINT, UINT*) { + return FR_OK; +} +FRESULT f_rename(const TCHAR*, const TCHAR*) { + return FR_OK; +} +FRESULT f_stat(const TCHAR*, FILINFO*) { + return FR_OK; +} +FRESULT f_sync(FIL*) { + return FR_OK; +} +FRESULT f_truncate(FIL*) { + return FR_OK; +} +FRESULT f_unlink(const TCHAR*) { + return FR_OK; +} +FRESULT f_write(FIL*, const void*, UINT, UINT*) { + return FR_OK; +} + +/* Debug */ +void __debug_log(const std::string&) {} diff --git a/firmware/test/application/test_file_reader.cpp b/firmware/test/application/test_file_reader.cpp index a1833b80..f5fa0c75 100644 --- a/firmware/test/application/test_file_reader.cpp +++ b/firmware/test/application/test_file_reader.cpp @@ -38,6 +38,20 @@ TEST_CASE("It can iterate file lines.") { CHECK_EQ(line_count, 3); } +TEST_CASE("It can iterate multiple times.") { + MockFile f{"abc\ndef\nhij"}; + BufferLineReader reader{f}; + int line_count = 0; + int line_count2 = 0; + for (const auto& line : reader) + ++line_count; + for (const auto& line : reader) + ++line_count2; + + CHECK_EQ(line_count, 3); + CHECK_EQ(line_count2, 3); +} + TEST_CASE("It can iterate file ending with newline.") { MockFile f{"abc\ndef\nhij\n"}; BufferLineReader reader{f}; @@ -133,6 +147,18 @@ TEST_CASE("It will split only empty columns.") { TEST_SUITE_END(); +TEST_CASE("count_lines returns 1 for single line") { + MockFile f{"abs"}; + BufferLineReader reader{f}; + CHECK_EQ(count_lines(reader), 1); +} + +TEST_CASE("count_lines returns 2 for 2 lines") { + MockFile f{"abs"}; + BufferLineReader reader{f}; + CHECK_EQ(count_lines(reader), 1); +} + /* Simple example of how to use this to read settings by lines. */ TEST_CASE("It can parse a settings file.") { MockFile f{"100,File.txt,5\n200,File2.txt,7"}; diff --git a/firmware/test/application/test_freqman_db.cpp b/firmware/test/application/test_freqman_db.cpp new file mode 100644 index 00000000..1e93b1a4 --- /dev/null +++ b/firmware/test/application/test_freqman_db.cpp @@ -0,0 +1,217 @@ +/* + * 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. + */ + +#include "doctest.h" +#include "freqman_db.hpp" + +TEST_SUITE_BEGIN("Freqman Parsing"); + +TEST_CASE("It can parse basic single freq entry.") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.", e)); + + CHECK_EQ(e.frequency_a, 123'000'000); + CHECK_EQ(e.description, "This is the description."); + CHECK_EQ(e.type, freqman_type::Single); +} + +TEST_CASE("It can parse basic range freq entry.") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "a=123000000,b=423000000,d=This is the description.", e)); + + CHECK_EQ(e.frequency_a, 123'000'000); + CHECK_EQ(e.frequency_b, 423'000'000); + CHECK_EQ(e.description, "This is the description."); + CHECK_EQ(e.type, freqman_type::Range); +} + +TEST_CASE("It can parse basic ham radio freq entry.") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "r=123000000,t=423000000,d=This is the description.", e)); + + CHECK_EQ(e.frequency_a, 123'000'000); + CHECK_EQ(e.frequency_b, 423'000'000); + CHECK_EQ(e.description, "This is the description."); + CHECK_EQ(e.type, freqman_type::HamRadio); +} + +TEST_CASE("It can parse modulation") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=AM", e)); + CHECK_EQ(e.modulation, 0); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=NFM", e)); + CHECK_EQ(e.modulation, 1); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=WFM", e)); + CHECK_EQ(e.modulation, 2); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=SPEC", e)); + CHECK_EQ(e.modulation, 3); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=FOO", e)); + CHECK_EQ(e.modulation, freqman_invalid_index); +} + +TEST_CASE("It can parse bandwidth") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=AM,bw=DSB 6k", e)); + CHECK_EQ(e.bandwidth, 1); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,bw=DSB 6k", e)); + // Modulation wasn't set. + CHECK_EQ(e.bandwidth, freqman_invalid_index); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=WFM,bw=50k", e)); + // Invalid bandwidth value. + CHECK_EQ(e.bandwidth, freqman_invalid_index); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=SPEC,bw=16k", e)); + CHECK_EQ(e.modulation, 3); + + REQUIRE( + parse_freqman_entry( + "f=123000000,m=NFM,bw=11k,d=This is the description.", e)); + CHECK_EQ(e.modulation, 1); +} + +TEST_CASE("It can parse frequency step") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,s=0.1kHz", e)); + CHECK_EQ(e.step, 0); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,s=50kHz", e)); + CHECK_EQ(e.step, 11); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,s=FOO", e)); + CHECK_EQ(e.step, freqman_invalid_index); +} + +TEST_CASE("It can parse tone freq") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,c=0.0", e)); + CHECK_EQ(e.tone, 0); + + REQUIRE( + parse_freqman_entry( + "f=123000000,c=67.0,d=This is the description.", e)); + CHECK_EQ(e.tone, 1); + + REQUIRE( + parse_freqman_entry( + "f=123000000,c=67,d=This is the description.", e)); + // Fractional can be omitted. + CHECK_EQ(e.tone, 1); + + REQUIRE( + parse_freqman_entry( + "f=123000000,c=69.33,d=This is the description.", e)); + // Fractional extra digits can be omitted. + CHECK_EQ(e.tone, 2); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,c=72", e)); + // Should choose nearest. + CHECK_EQ(e.tone, 3); +} + +#if 0 // New tables for a future PR. +TEST_CASE("It can parse modulation") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=AM", e)); + CHECK_EQ(e.modulation, freqman_modulation::AM); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=NFM", e)); + CHECK_EQ(e.modulation, freqman_modulation::NFM); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=WFM", e)); + CHECK_EQ(e.modulation, freqman_modulation::WFM); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=SPEC", e)); + CHECK_EQ(e.modulation, freqman_modulation::SPEC); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,m=FOO", e)); + CHECK_EQ(e.modulation, freqman_modulation::Unknown); +} + +TEST_CASE("It can parse frequency step") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,s=0.1kHz", e)); + CHECK_EQ(e.step, freqman_step::_100Hz); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,s=50kHz", e)); + CHECK_EQ(e.step, freqman_step::_50kHz); + + REQUIRE( + parse_freqman_entry( + "f=123000000,d=This is the description.,s=FOO", e)); + CHECK_EQ(e.step, freqman_step::Unknown); +} +#endif + +TEST_SUITE_END();