diff --git a/firmware/application/apps/ui_freqman.cpp b/firmware/application/apps/ui_freqman.cpp index 453cda2b..47f971c0 100644 --- a/firmware/application/apps/ui_freqman.cpp +++ b/firmware/application/apps/ui_freqman.cpp @@ -412,7 +412,8 @@ void FrequencyEditView::refresh_ui() { auto is_range = entry_.type == freqman_type::Range; auto is_ham = entry_.type == freqman_type::HamRadio; - auto has_freq_b = is_range || is_ham; + auto is_repeater = entry_.type == freqman_type::Repeater; + auto has_freq_b = is_range || is_ham || is_repeater; field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey); field_step.set_style(is_range ? &Styles::white : &Styles::grey); diff --git a/firmware/application/apps/ui_freqman.hpp b/firmware/application/apps/ui_freqman.hpp index 03a7f021..0ece16a3 100644 --- a/firmware/application/apps/ui_freqman.hpp +++ b/firmware/application/apps/ui_freqman.hpp @@ -220,7 +220,8 @@ class FrequencyEditView : public View { {"Single", 0}, {"Range", 1}, {"HamRadio", 2}, - {"Raw", 3}, + {"Repeater", 3}, + {"Raw", 4}, }}; FrequencyField field_freq_a{{13 * 8, 4 * 16}}; diff --git a/firmware/application/apps/ui_recon.cpp b/firmware/application/apps/ui_recon.cpp index cdfc63d7..1dafadf0 100644 --- a/firmware/application/apps/ui_recon.cpp +++ b/firmware/application/apps/ui_recon.cpp @@ -22,13 +22,24 @@ */ #include "ui_recon.hpp" -#include "ui_fileman.hpp" #include "ui_freqman.hpp" #include "capture_app.hpp" #include "convert.hpp" #include "file.hpp" #include "file_reader.hpp" #include "tone_key.hpp" +#include "replay_app.hpp" +#include "string_format.hpp" +#include "ui_fileman.hpp" +#include "io_file.hpp" +#include "io_convert.hpp" +#include "oversample.hpp" +#include "baseband_api.hpp" +#include "metadata_file.hpp" +#include "portapack.hpp" +#include "portapack_persistent_memory.hpp" +#include "utility.hpp" +#include "replay_thread.hpp" using namespace portapack; using namespace tonekey; @@ -37,6 +48,30 @@ namespace fs = std::filesystem; namespace ui { +void ReconView::reload_restart_recon() { + frequency_file_load(); + if (frequency_list.size() > 0) { + if (fwd) { + button_dir.set_text("FW>"); + } else { + button_dir.set_text(" FREQMAN_MAX_PER_FILE) { + file_name.set_style(&Styles::yellow); + } +} + void ReconView::check_update_ranges_from_current() { if (frequency_list.size() && current_is_valid() && current_entry().type == freqman_type::Range) { if (update_ranges && !manual_mode) { @@ -75,8 +110,12 @@ void ReconView::recon_stop_recording() { button_audio_app.set_text("AUDIO"); button_audio_app.set_style(&Styles::white); record_view->stop(); - button_config.set_style(&Styles::white); // disable config while recording as it's causing an IO error pop up at exit + button_config.set_style(&Styles::white); is_recording = false; + // repeater mode + if (persistent_memory::recon_repeat_recorded()) { + start_repeat(); + } } } @@ -111,6 +150,9 @@ void ReconView::update_description() { case freqman_type::HamRadio: description = "H: "; break; + case freqman_type::Repeater: + description = "L: "; + break; default: description = "S: "; } @@ -182,6 +224,7 @@ void ReconView::load_persisted_settings() { load_freqs = persistent_memory::recon_load_freqs(); load_ranges = persistent_memory::recon_load_ranges(); load_hamradios = persistent_memory::recon_load_hamradios(); + load_repeaters = persistent_memory::recon_load_repeaters(); update_ranges = persistent_memory::recon_update_ranges_when_recon(); auto_record_locked = persistent_memory::recon_auto_record_locked(); } @@ -275,9 +318,20 @@ ReconView::~ReconView() { ReconView::ReconView(NavigationView& nav) : nav_{nav} { chrono_start = chTimeNow(); + + tx_view.hidden(true); + + // set record View record_view = std::make_unique(Rect{0, 0, 30 * 8, 1 * 16}, - u"AUTO_AUDIO_", u"AUDIO", + u"AUTO_AUDIO", u"AUDIO", RecordView::FileType::WAV, 4096, 4); + record_view->set_filename_date_frequency(true); + record_view->set_auto_trim(false); + record_view->hidden(true); + record_view->on_error = [&nav](std::string message) { + nav.display_modal("Error", message); + }; + add_children({&labels, &field_lna, &field_vga, @@ -314,13 +368,9 @@ ReconView::ReconView(NavigationView& nav) &button_restart, &button_mic_app, &button_remove, - record_view.get()}); - - record_view->hidden(true); - record_view->set_filename_date_frequency(true); - record_view->on_error = [&nav](std::string message) { - nav.display_modal("Error", message); - }; + record_view.get(), + &progressbar, + &tx_view}); def_step = 0; load_persisted_settings(); @@ -566,27 +616,7 @@ ReconView::ReconView(NavigationView& nav) }; button_restart.on_select = [this](Button&) { - frequency_file_load(); - if (frequency_list.size() > 0) { - if (fwd) { - button_dir.set_text("FW>"); - } else { - button_dir.set_text(" FREQMAN_MAX_PER_FILE) { - file_name.set_style(&Styles::yellow); - } + reload_restart_recon(); }; button_add.on_select = [this](ButtonWithEncoder&) { @@ -704,6 +734,9 @@ ReconView::ReconView(NavigationView& nav) change_mode(AM_MODULATION); // start on AM. field_mode.set_by_value(AM_MODULATION); // reflect the mode into the manual selector + // tx progress bar + progressbar.hidden(true); + if (filedelete) { delete_file(freq_file_path); } @@ -741,7 +774,8 @@ void ReconView::frequency_file_load() { freqman_load_options options{ .load_freqs = load_freqs, .load_ranges = load_ranges, - .load_hamradios = load_hamradios}; + .load_hamradios = load_hamradios, + .load_repeaters = load_repeaters}; if (!load_freqman_file(file_input, frequency_list, options) || frequency_list.empty()) { file_name.set_style(&Styles::red); desc_cycle.set("...empty file..."); @@ -776,6 +810,12 @@ void ReconView::frequency_file_load() { } void ReconView::on_statistics_update(const ChannelStatistics& statistics) { + if (recon_tx) + return; + + if (is_repeat_active()) + return; + chrono_end = chTimeNow(); systime_t time_interval = chrono_end - chrono_start; chrono_start = chrono_end; @@ -835,9 +875,9 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { // contents of a possible recon_start_recording(), but not yet since it's only called once if (auto_record_locked && !is_recording) { button_audio_app.set_style(&Styles::red); - if (field_mode.selected_index_value() == SPEC_MODULATION) + if (field_mode.selected_index_value() == SPEC_MODULATION) { button_audio_app.set_text("RAW REC"); - else + } else button_audio_app.set_text("WAV REC"); record_view->start(); button_config.set_style(&Styles::light_grey); // disable config while recording as it's causing an IO error pop up at exit @@ -956,6 +996,26 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { } } } + } else if (current_entry().type == freqman_type::Repeater) { + // repeater is like single, we only listen on frequency_a and then jump to next entry + if ((fwd && stepper == 0) || stepper > 0) { // forward + current_index++; + entry_has_changed = true; + // looping + if ((uint32_t)current_index >= frequency_list.size()) { + has_looped = true; + current_index = 0; + } + } else if ((!fwd && stepper == 0) || stepper < 0) { + // reverse + current_index--; + entry_has_changed = true; + // if previous if under the list => go back from end + if (current_index < 0) { + has_looped = true; + current_index = frequency_list.size() - 1; + } + } } // set index to boundary if !continuous if (has_looped && !continuous) { @@ -984,6 +1044,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { if (entry_has_changed) { timer = 0; switch (current_entry().type) { + case freqman_type::Repeater: case freqman_type::Single: freq = current_entry().frequency_a; break; @@ -1017,7 +1078,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { if (stepper > 0) stepper--; } // if( recon || stepper != 0 || index_stepper != 0 ) } // if (frequency_list.size() > 0 ) - } /* on_statistic_updates */ + } /* on_statistics_updates */ } handle_retune(); recon_redraw(); @@ -1080,28 +1141,39 @@ void ReconView::on_stepper_delta(int32_t v) { } size_t ReconView::change_mode(freqman_index_t new_mod) { + if (recon_tx || is_repeat_active()) + return 0; field_mode.on_change = [this](size_t, OptionsField::value_t) {}; field_bw.on_change = [this](size_t, OptionsField::value_t) {}; recon_stop_recording(); - remove_child(record_view.get()); - record_view.reset(); - if (new_mod == SPEC_MODULATION) { + if (record_view != nullptr) { + remove_child(record_view.get()); + record_view.reset(); + } + if (persistent_memory::recon_repeat_recorded()) { + record_view = std::make_unique(Rect{0, 0, 30 * 8, 1 * 16}, + u"RECON_REPEAT.C16", u"CAPTURES", + RecordView::FileType::RawS16, 16384, 3); + record_view->set_filename_as_is(true); + } else if (new_mod == SPEC_MODULATION) { audio::output::stop(); record_view = std::make_unique(Rect{0, 0, 30 * 8, 1 * 16}, - u"AUTO_RAW_", u"CAPTURES", + u"AUTO_RAW", u"CAPTURES", RecordView::FileType::RawS16, 16384, 3); } else { record_view = std::make_unique(Rect{0, 0, 30 * 8, 1 * 16}, - u"AUTO_AUDIO_", u"AUDIO", + u"AUTO_AUDIO", u"AUDIO", RecordView::FileType::WAV, 4096, 4); + record_view->set_filename_date_frequency(true); } + record_view->set_auto_trim(false); add_child(record_view.get()); record_view->hidden(true); - record_view->set_filename_date_frequency(true); record_view->on_error = [this](std::string message) { nav_.display_modal("Error", message); }; receiver_model.disable(); + transmitter_model.disable(); baseband::shutdown(); size_t recording_sampling_rate = 0; switch (new_mod) { @@ -1144,7 +1216,6 @@ size_t ReconView::change_mode(freqman_index_t new_mod) { field_bw.on_change = [this](size_t, OptionsField::value_t sampling_rate) { // record_view determines the correct oversampling to apply and returns the actual sample rate. auto actual_sampling_rate = record_view->set_sampling_rate(sampling_rate); - // The radio needs to know the effective sampling rate. receiver_model.set_sampling_rate(actual_sampling_rate); receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate)); @@ -1222,4 +1293,132 @@ void ReconView::handle_remove_current_item() { update_description(); } +void ReconView::on_repeat_tx_progress(const uint32_t progress) { + progressbar.set_value(progress); +} + +void ReconView::repeat_file_error(const std::filesystem::path& path, const std::string& message) { + nav_.display_modal("Error", "Error opening file \n" + path.string() + "\n" + message); +} + +bool ReconView::is_repeat_active() const { + return replay_thread != nullptr; +} + +void ReconView::start_repeat() { + // Prepare to send a file. + std::filesystem::path rawfile = u"/" + repeat_rec_path + u"/" + repeat_rec_file; + std::filesystem::path rawmeta = u"/" + repeat_rec_path + u"/" + repeat_rec_meta; + + if (recon_tx == false) { + recon_tx = true; + + if (record_view != nullptr) { + record_view->stop(); + remove_child(record_view.get()); + record_view.reset(); + } + + receiver_model.disable(); + transmitter_model.disable(); + baseband::shutdown(); + + baseband::run_image(portapack::spi_flash::image_tag_replay); + + size_t rawsize = 0; + { + File capture_file; + auto error = capture_file.open(rawfile); + if (error) { + repeat_file_error(rawfile, "Can't open file to send for size"); + return; + } + rawsize = capture_file.size(); + } + + // Reset the transmit progress bar. + uint8_t sample_size = std::filesystem::capture_file_sample_size(rawfile); + progressbar.set_value(0); + progressbar.set_max(rawsize * sizeof(complex16_t) / sample_size); + progressbar.hidden(false); + + auto metadata = read_metadata_file(rawmeta); + // If no metadata found, fallback to the TX frequency. + if (!metadata) { + metadata = {freq, 500'000}; + repeat_file_error(rawmeta, "Can't open file to read meta, using default rx_freq,500'000"); + } + + // Update the sample rate in proc_replay baseband. + baseband::set_sample_rate(metadata->sample_rate, + get_oversample_rate(metadata->sample_rate)); + + transmitter_model.set_sampling_rate(get_actual_sample_rate(metadata->sample_rate)); + transmitter_model.set_baseband_bandwidth(metadata->sample_rate <= 500'000 ? 1'750'000 : 2'500'000); // TX LPF min 1M75 for SR <=500K, and 2M5 (by experimental test) for SR >500K + + // set TX to repeater TX freq if entry is Repeater, else use recorded one + if (current_entry().type == freqman_type::Repeater) { + transmitter_model.set_target_frequency(current_entry().frequency_b); + } else { + transmitter_model.set_target_frequency(metadata->center_frequency); + } + + // set TX powers and enable transmitter + transmitter_model.set_tx_gain(persistent_memory::recon_repeat_gain()); + transmitter_model.set_rf_amp(persistent_memory::recon_repeat_amp()); + transmitter_model.enable(); + } + + // clear replay thread and set reader + replay_thread.reset(); + auto reader = std::make_unique(); + auto error = reader->open(rawfile); + if (error) { + repeat_file_error(rawfile, "Can't open file to send to thread"); + return; + } + repeat_ready_signal = true; + repeat_cur_rep++; + + // ReplayThread starts immediately on construction; must be set before creating. + replay_thread = std::make_unique( + std::move(reader), + /* read_size */ repeat_read_size, + /* buffer_count */ repeat_buffer_count, + &repeat_ready_signal, + [](uint32_t return_code) { + ReplayThreadDoneMessage message{return_code}; + EventDispatcher::send_message(message); + }); +} + +void ReconView::stop_repeat(const bool do_loop) { + repeat_ready_signal = false; + + if (is_repeat_active()) { + replay_thread.reset(); + transmitter_model.disable(); + } + + // repeat transmit if current number of repetitions (repeat_cur_rep) is < recon configured number of repetitions (recon_repeat_nb) + if (do_loop && repeat_cur_rep < persistent_memory::recon_repeat_nb()) { + start_repeat(); + } else { + repeat_cur_rep = 0; + recon_tx = false; + reload_restart_recon(); + progressbar.hidden(true); + set_dirty(); // fix progressbar no hiding + } +} + +void ReconView::handle_repeat_thread_done(const uint32_t return_code) { + if (return_code == ReplayThread::END_OF_FILE) { + stop_repeat(true); + } else if (return_code == ReplayThread::READ_ERROR) { + stop_repeat(false); + repeat_file_error(u"/" + repeat_rec_path + u"/" + repeat_rec_file, "Can't open file to send."); + } +} + } /* namespace ui */ diff --git a/firmware/application/apps/ui_recon.hpp b/firmware/application/apps/ui_recon.hpp index 477524c4..141db39f 100644 --- a/firmware/application/apps/ui_recon.hpp +++ b/firmware/application/apps/ui_recon.hpp @@ -41,10 +41,20 @@ #include "app_settings.hpp" #include "radio_state.hpp" #include "ui_recon_settings.hpp" +#include "ui_transmitter.hpp" +#include "replay_thread.hpp" +#include "metadata_file.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_freq_field.hpp" +#include "ui_spectrum.hpp" + +#include +#include namespace ui { -#define RECON_CFG_FILE "SETTINGS/recon.cfg" +#define RECON_CFG_FILE u"SETTINGS/recon.cfg" enum class recon_mode : uint8_t { Recon, @@ -66,10 +76,17 @@ class ReconView : public View { private: NavigationView& nav_; - RxRadioState radio_state_{}; + RxRadioState rx_radio_state_{}; + TxRadioState tx_radio_state_{ + 0 /* frequency */, + 1750000 /* bandwidth */, + 500000 /* sampling rate */ + }; app_settings::SettingsManager settings_{ - "rx_recon"sv, app_settings::Mode::RX}; + "rx_tx_recon"sv, app_settings::Mode::RX_TX}; + ; + void reload_restart_recon(); void check_update_ranges_from_current(); void set_loop_config(bool v); void clear_freqlist_for_ui_action(); @@ -125,6 +142,7 @@ class ReconView : public View { bool load_freqs{true}; bool load_ranges{true}; bool load_hamradios{true}; + bool load_repeaters{true}; bool update_ranges{true}; bool fwd{true}; bool recon{true}; @@ -163,6 +181,25 @@ class ReconView : public View { systime_t chrono_start{}; systime_t chrono_end{}; + const std::filesystem::path repeat_rec_file = u"RECON_REPEAT.C16"; + const std::filesystem::path repeat_rec_meta = u"RECON_REPEAT.TXT"; + const std::filesystem::path repeat_rec_path = u"CAPTURES"; + const size_t repeat_read_size{16384}; + const size_t repeat_buffer_count{3}; + int8_t repeat_cur_rep = 0; + int64_t repeat_sample_rate = 0; + static constexpr uint32_t repeat_bandwidth = 2500000; + void on_repeat_tx_progress(const uint32_t progress); + void start_repeat(); + void stop_repeat(const bool do_loop); + bool is_repeat_active() const; + void handle_repeat_thread_done(const uint32_t return_code); + void repeat_file_error(const std::filesystem::path& path, const std::string& message); + std::filesystem::path repeat_file_path{}; + std::unique_ptr replay_thread{}; + bool repeat_ready_signal{false}; + bool recon_tx{false}; + // Persisted settings. SettingsStore ui_settings{ "recon"sv, @@ -354,17 +391,47 @@ class ReconView : public View { {168, (35 * 8) - 4, 72, 28}, ""}; + ProgressBar progressbar{ + {0 * 8, SCREEN_H / 2 - 16, SCREEN_W, 32}}; + + TransmitterView2 tx_view{ + {11 * 8, 2 * 16}, + /*short_ui*/ true}; + MessageHandlerRegistration message_handler_coded_squelch{ Message::ID::CodedSquelch, [this](const Message* const p) { const auto message = *reinterpret_cast(p); - this->handle_coded_squelch(message.value); + handle_coded_squelch(message.value); }}; MessageHandlerRegistration message_handler_stats{ Message::ID::ChannelStatistics, [this](const Message* const p) { - this->on_statistics_update(static_cast(p)->statistics); + on_statistics_update(static_cast(p)->statistics); + }}; + + MessageHandlerRegistration message_handler_replay_thread_error{ + Message::ID::ReplayThreadDone, + [this](const Message* p) { + auto message = *reinterpret_cast(p); + handle_repeat_thread_done(message.return_code); + }}; + + MessageHandlerRegistration message_handler_fifo_signal{ + Message::ID::RequestSignal, + [this](const Message* p) { + auto message = static_cast(p); + if (message->signal == RequestSignalMessage::Signal::FillRequest) { + repeat_ready_signal = true; + } + }}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* p) { + auto message = *reinterpret_cast(p); + on_repeat_tx_progress(message.progress); }}; }; diff --git a/firmware/application/apps/ui_recon_settings.cpp b/firmware/application/apps/ui_recon_settings.cpp index 00a44998..d400a8da 100644 --- a/firmware/application/apps/ui_recon_settings.cpp +++ b/firmware/application/apps/ui_recon_settings.cpp @@ -99,8 +99,13 @@ void ReconSetupViewMore::save() { persistent_memory::set_recon_load_freqs(checkbox_load_freqs.value()); persistent_memory::set_recon_load_ranges(checkbox_load_ranges.value()); persistent_memory::set_recon_load_hamradios(checkbox_load_hamradios.value()); + persistent_memory::set_recon_load_repeaters(checkbox_load_repeaters.value()); persistent_memory::set_recon_update_ranges_when_recon(checkbox_update_ranges_when_recon.value()); persistent_memory::set_recon_auto_record_locked(checkbox_auto_record_locked.value()); + persistent_memory::set_recon_repeat_recorded(checkbox_repeat_recorded.value()); + persistent_memory::set_recon_repeat_nb(field_repeat_nb.value()); + persistent_memory::set_recon_repeat_amp(checkbox_repeat_amp.value()); + persistent_memory::set_recon_repeat_gain(field_repeat_gain.value()); }; void ReconSetupViewMain::focus() { @@ -113,16 +118,50 @@ ReconSetupViewMore::ReconSetupViewMore(NavigationView& nav, Rect parent_rect) hidden(true); add_children({&checkbox_load_freqs, + &checkbox_load_repeaters, &checkbox_load_ranges, &checkbox_load_hamradios, &checkbox_update_ranges_when_recon, - &checkbox_auto_record_locked}); + &checkbox_auto_record_locked, + &checkbox_repeat_recorded, + &text_repeat_nb, + &field_repeat_nb, + &checkbox_repeat_amp, + &text_repeat_gain, + &field_repeat_gain}); + + // tx options have to be in yellow to inform the users that activating them will make the device transmit + checkbox_repeat_recorded.set_style(&Styles::yellow); + text_repeat_nb.set_style(&Styles::yellow); + field_repeat_nb.set_style(&Styles::yellow); + checkbox_repeat_amp.set_style(&Styles::yellow); + text_repeat_gain.set_style(&Styles::yellow); + field_repeat_gain.set_style(&Styles::yellow); checkbox_load_freqs.set_value(persistent_memory::recon_load_freqs()); + checkbox_load_repeaters.set_value(persistent_memory::recon_load_repeaters()); checkbox_load_ranges.set_value(persistent_memory::recon_load_ranges()); checkbox_load_hamradios.set_value(persistent_memory::recon_load_hamradios()); checkbox_update_ranges_when_recon.set_value(persistent_memory::recon_update_ranges_when_recon()); checkbox_auto_record_locked.set_value(persistent_memory::recon_auto_record_locked()); + checkbox_repeat_recorded.set_value(persistent_memory::recon_repeat_recorded()); + checkbox_repeat_amp.set_value(persistent_memory::recon_repeat_amp()); + field_repeat_nb.set_value(persistent_memory::recon_repeat_nb()); + field_repeat_gain.set_value(persistent_memory::recon_repeat_gain()); + + // tx warning modal + checkbox_repeat_recorded.on_select = [this, &nav](Checkbox&, bool v) { + if (v) { + nav.display_modal( + "TX WARNING", + "This activate TX ability\nin Recon !", + YESNO, + [this, &nav](bool choice) { + if (!choice) + checkbox_repeat_recorded.set_value(choice); + }); + } + }; }; void ReconSetupViewMore::focus() { diff --git a/firmware/application/apps/ui_recon_settings.hpp b/firmware/application/apps/ui_recon_settings.hpp index 27bd7ab4..01ae4331 100644 --- a/firmware/application/apps/ui_recon_settings.hpp +++ b/firmware/application/apps/ui_recon_settings.hpp @@ -30,6 +30,7 @@ #include "ui_tabview.hpp" #include "ui_navigation.hpp" #include "string_format.hpp" +#include "ui_styles.hpp" // 1Mhz helper #ifdef OneMHz @@ -116,26 +117,70 @@ class ReconSetupViewMore : public View { Checkbox checkbox_load_freqs{ {1 * 8, 12}, 3, - "input: load freqs"}; + "load freq", + true}; + + Checkbox checkbox_load_repeaters{ + {14 * 8, 12}, + 3, + "load repeater", + true}; Checkbox checkbox_load_ranges{ {1 * 8, 42}, 3, - "input: load ranges"}; + "load range", + true}; Checkbox checkbox_load_hamradios{ {1 * 8, 72}, 3, - "input: load hamradios"}; + "load hamradio", + true}; Checkbox checkbox_update_ranges_when_recon{ {1 * 8, 102}, 3, "auto update m-ranges"}; + Checkbox checkbox_auto_record_locked{ {1 * 8, 132}, 3, "record locked periods"}; + + Checkbox checkbox_repeat_recorded{ + {1 * 8, 162}, + 3, + "repeater,"}; + + Text text_repeat_nb{ + {14 * 8, 165, 3 * 8, 22}, + "nb:"}; + + NumberField field_repeat_nb{ + {18 * 8, 165}, + 2, + {1, 99}, + 1, + ' ', + }; + + Checkbox checkbox_repeat_amp{ + {1 * 8, 192}, + 3, + "AMP,"}; + + Text text_repeat_gain{ + {10 * 8, 196, 5 * 8, 22}, + "GAIN:"}; + + NumberField field_repeat_gain{ + {16 * 8, 196}, + 2, + {0, 47}, + 1, + ' ', + }; }; class ReconSetupView : public View { diff --git a/firmware/application/apps/ui_scanner.cpp b/firmware/application/apps/ui_scanner.cpp index 2f0718ba..902f697d 100644 --- a/firmware/application/apps/ui_scanner.cpp +++ b/firmware/application/apps/ui_scanner.cpp @@ -564,6 +564,7 @@ void ScannerView::frequency_file_load(const fs::path& path) { def_step_index = entry.step; switch (entry.type) { + case freqman_type::Repeater: case freqman_type::Single: entries.push_back({entry.frequency_a, entry.description}); break; diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp index b3ef011f..6bbc3d77 100644 --- a/firmware/application/freqman_db.cpp +++ b/firmware/application/freqman_db.cpp @@ -185,6 +185,8 @@ bool operator==(const freqman_entry& lhs, const freqman_entry& rhs) { } else if (lhs.type == freqman_type::HamRadio) { equal = lhs.frequency_b == rhs.frequency_b && lhs.tone == rhs.tone; + } else if (lhs.type == freqman_type::Repeater) { + equal = lhs.frequency_b == rhs.frequency_b; } return equal; @@ -248,6 +250,10 @@ std::string pretty_string(const freqman_entry& entry, size_t max_length) { str = "R:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" + to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description; break; + case freqman_type::Repeater: + str = "L:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" + + to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description; + break; case freqman_type::Raw: str = entry.description; break; @@ -292,6 +298,10 @@ std::string to_freqman_string(const freqman_entry& entry) { if (is_valid(entry.tone)) append_field("c", tonekey::tone_key_value_string(entry.tone)); break; + case freqman_type::Repeater: + append_field("l", to_string_dec_uint(entry.frequency_a)); + append_field("t", to_string_dec_uint(entry.frequency_b)); + break; case freqman_type::Raw: return entry.description; default: @@ -372,16 +382,18 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { parse_int(value, entry.frequency_a); } else if (key == "m") { entry.modulation = find_by_name(freqman_modulations, value); - } else if (key == "r") { + } else if (key == "r") { // HamRadio relay receive freq entry.type = freqman_type::HamRadio; parse_int(value, entry.frequency_a); + } else if (key == "l") { // Portapack Repeater mode listen freq. Used as a single freq if Repeater mode isn't active + entry.type = freqman_type::Repeater; + parse_int(value, entry.frequency_a); } else if (key == "s") { entry.step = find_by_name(freqman_steps_short, value); - } else if (key == "t") { + } else if (key == "t") { // Tx freq: scanned as a single freq in HamRadio mode, used as TX freq in Repeater mode and ignored by the scanner parse_int(value, entry.frequency_b); } } - return is_valid(entry); } @@ -400,7 +412,8 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio if (entry.type == freqman_type::Unknown || (entry.type == freqman_type::Single && !options.load_freqs) || (entry.type == freqman_type::Range && !options.load_ranges) || - (entry.type == freqman_type::HamRadio && !options.load_hamradios)) { + (entry.type == freqman_type::HamRadio && !options.load_hamradios) || + (entry.type == freqman_type::Repeater && !options.load_repeaters)) { continue; } @@ -433,8 +446,8 @@ bool is_valid(const freqman_entry& entry) { if (entry.frequency_a == 0) return false; - // Frequency B must be set for type Range or Ham Radio - if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) { + // Frequency B must be set for type Range or HamRadio or Repeater + if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio || entry.type == freqman_type::Repeater) { if (entry.frequency_b == 0) return false; } diff --git a/firmware/application/freqman_db.hpp b/firmware/application/freqman_db.hpp index a93bd6f6..4f31ea04 100644 --- a/firmware/application/freqman_db.hpp +++ b/firmware/application/freqman_db.hpp @@ -56,6 +56,7 @@ enum class freqman_type : uint8_t { Single, // f= Range, // a=,b= HamRadio, // r=,t= + Repeater, // l=,t= Raw, // line content in description Unknown, }; @@ -167,6 +168,7 @@ struct freqman_load_options { bool load_freqs{true}; bool load_ranges{true}; bool load_hamradios{true}; + bool load_repeaters{true}; }; using freqman_entry_ptr = std::unique_ptr; @@ -277,4 +279,4 @@ class FreqmanDB { bool read_raw_{true}; }; -#endif /* __FREQMAN_DB_H__ */ \ No newline at end of file +#endif /* __FREQMAN_DB_H__ */ diff --git a/firmware/application/ui_record_view.cpp b/firmware/application/ui_record_view.cpp index 0478d97f..3c0a37b9 100644 --- a/firmware/application/ui_record_view.cpp +++ b/firmware/application/ui_record_view.cpp @@ -145,6 +145,15 @@ OversampleRate RecordView::get_oversample_rate(uint32_t sample_rate) { // Setter for datetime and frequency filename void RecordView::set_filename_date_frequency(bool set) { filename_date_frequency = set; + if (set) + filename_as_is = false; +} + +// Setter for leaving the filename untouched +void RecordView::set_filename_as_is(bool set) { + filename_as_is = set; + if (set) + filename_date_frequency = false; } bool RecordView::is_active() const { @@ -186,9 +195,11 @@ void RecordView::start() { base_path = filename_stem_pattern.string() + "_" + date_time + "_" + trim(to_string_freq(receiver_model.target_frequency())) + "Hz"; base_path = folder / base_path; - } else { + } else if (filename_as_is) { + base_path = filename_stem_pattern.string(); + base_path = folder / base_path; + } else base_path = next_filename_matching_pattern(folder / filename_stem_pattern); - } if (base_path.empty()) { return; diff --git a/firmware/application/ui_record_view.hpp b/firmware/application/ui_record_view.hpp index c062a454..785f1428 100644 --- a/firmware/application/ui_record_view.hpp +++ b/firmware/application/ui_record_view.hpp @@ -73,6 +73,7 @@ class RecordView : public View { bool is_active() const; void set_filename_date_frequency(bool set); + void set_filename_as_is(bool set); private: void toggle(); @@ -91,6 +92,7 @@ class RecordView : public View { // Time Stamp bool filename_date_frequency = false; + bool filename_as_is = false; rtc::RTC datetime{}; const std::filesystem::path filename_stem_pattern; diff --git a/firmware/common/portapack_persistent_memory.cpp b/firmware/common/portapack_persistent_memory.cpp index 6afafea7..9e4db7aa 100644 --- a/firmware/common/portapack_persistent_memory.cpp +++ b/firmware/common/portapack_persistent_memory.cpp @@ -195,6 +195,8 @@ struct data_t { // Recon App uint64_t recon_config; + int8_t recon_repeat_nb; + int8_t recon_repeat_gain; // enable or disable converter bool converter; @@ -257,7 +259,10 @@ struct data_t { tone_mix(tone_mix_reset_value), hardware_config(0), + recon_config(0), + recon_repeat_nb(0), + recon_repeat_gain(0), converter(false), updown_converter(false), @@ -391,10 +396,15 @@ void defaults() { set_recon_continuous(true); set_recon_clear_output(false); set_recon_load_freqs(true); + set_recon_load_repeaters(true); set_recon_load_ranges(true); set_recon_update_ranges_when_recon(true); set_recon_load_hamradios(true); set_recon_match_mode(0); + set_recon_repeat_recorded(false); + set_recon_repeat_amp(false); + set_recon_repeat_gain(35); + set_recon_repeat_nb(3); set_config_sdcard_high_speed_io(false, true); } @@ -747,6 +757,21 @@ bool recon_match_mode() { bool recon_auto_record_locked() { return (data->recon_config & 0x00400000UL) ? true : false; } +bool recon_repeat_recorded() { + return (data->recon_config & 0x00200000UL) ? true : false; +} +int8_t recon_repeat_nb() { + return data->recon_repeat_nb; +} +int8_t recon_repeat_gain() { + return data->recon_repeat_gain; +} +bool recon_repeat_amp() { + return (data->recon_config & 0x00100000UL) ? true : false; +} +bool recon_load_repeaters() { + return (data->recon_config & 0x00080000UL) ? true : false; +} void set_recon_autosave_freqs(const bool v) { data->recon_config = (data->recon_config & ~0x80000000UL) | (v << 31); @@ -778,6 +803,21 @@ void set_recon_match_mode(const bool v) { void set_recon_auto_record_locked(const bool v) { data->recon_config = (data->recon_config & ~0x00400000UL) | (v << 22); } +void set_recon_repeat_recorded(const bool v) { + data->recon_config = (data->recon_config & ~0x00200000UL) | (v << 21); +} +void set_recon_repeat_nb(const int8_t v) { + data->recon_repeat_nb = v; +} +void set_recon_repeat_gain(const int8_t v) { + data->recon_repeat_gain = v; +} +void set_recon_repeat_amp(const bool v) { + data->recon_config = (data->recon_config & ~0x00100000UL) | (v << 20); +} +void set_recon_load_repeaters(const bool v) { + data->recon_config = (data->recon_config & ~0x00080000UL) | (v << 19); +} /* UI Config 2 */ bool ui_hide_speaker() { diff --git a/firmware/common/portapack_persistent_memory.hpp b/firmware/common/portapack_persistent_memory.hpp index 2760322b..3c77ba7f 100644 --- a/firmware/common/portapack_persistent_memory.hpp +++ b/firmware/common/portapack_persistent_memory.hpp @@ -245,9 +245,14 @@ bool recon_autostart_recon(); bool recon_continuous(); bool recon_clear_output(); bool recon_load_freqs(); +bool recon_load_repeaters(); bool recon_load_ranges(); bool recon_update_ranges_when_recon(); bool recon_auto_record_locked(); +bool recon_repeat_recorded(); +int8_t recon_repeat_nb(); +int8_t recon_repeat_gain(); +bool recon_repeat_amp(); bool recon_load_hamradios(); bool recon_match_mode(); void set_recon_autosave_freqs(const bool v); @@ -258,7 +263,12 @@ void set_recon_load_freqs(const bool v); void set_recon_load_ranges(const bool v); void set_recon_update_ranges_when_recon(const bool v); void set_recon_auto_record_locked(const bool v); +void set_recon_repeat_recorded(const bool v); +void set_recon_repeat_nb(const int8_t v); +void set_recon_repeat_gain(const int8_t v); +void set_recon_repeat_amp(const bool v); void set_recon_load_hamradios(const bool v); +void set_recon_load_repeaters(const bool v); void set_recon_match_mode(const bool v); /* UI Config 2 */ diff --git a/firmware/test/application/test_freqman_db.cpp b/firmware/test/application/test_freqman_db.cpp index 5d3b308b..d2348021 100644 --- a/firmware/test/application/test_freqman_db.cpp +++ b/firmware/test/application/test_freqman_db.cpp @@ -59,6 +59,17 @@ TEST_CASE("It can parse basic ham radio freq entry.") { CHECK_EQ(e.type, freqman_type::HamRadio); } +TEST_CASE("It can parse repeater entry.") { + freqman_entry e; + REQUIRE( + parse_freqman_entry( + "l=123000000,t=123000500,d=This is the description.", e)); + CHECK_EQ(e.frequency_a, 123'000'000); + CHECK_EQ(e.frequency_b, 123'000'500); + CHECK_EQ(e.description, "This is the description."); + CHECK_EQ(e.type, freqman_type::Repeater); +} + TEST_CASE("It can parse modulation") { freqman_entry e; REQUIRE( @@ -195,6 +206,16 @@ TEST_CASE("It can serialize basic HamRadio entry") { CHECK(str == "r=123456000,t=423456000,d=Foobar"); } +TEST_CASE("It can serialize basic Repeater entry") { + auto str = to_freqman_string(freqman_entry{ + .frequency_a = 123'456'000, + .frequency_b = 423'456'000, + .description = "Foobar", + .type = freqman_type::Repeater, + }); + CHECK(str == "l=123456000,t=423456000,d=Foobar"); +} + // New tables for a future PR. /* TEST_CASE("It can parse modulation") {