diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 2def6164..80fc9c56 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -267,6 +267,7 @@ set(CPPSRC apps/lge_app.cpp apps/pocsag_app.cpp apps/replay_app.cpp + apps/ui_playlist.cpp apps/gps_sim_app.cpp apps/soundboard_app.cpp apps/tpms_app.cpp diff --git a/firmware/application/apps/ui_playlist.cpp b/firmware/application/apps/ui_playlist.cpp new file mode 100644 index 00000000..835cf5b4 --- /dev/null +++ b/firmware/application/apps/ui_playlist.cpp @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * Copyleft (ↄ) 2022 NotPike + * + * 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_playlist.hpp" +#include "string_format.hpp" + +#include "ui_fileman.hpp" +#include "io_file.hpp" + +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "portapack_persistent_memory.hpp" +#include + +using namespace portapack; + +namespace ui { + +void PlaylistView::set_ready() { + ready_signal = true; +} + + + void PlaylistView::load_file(std::filesystem::path playlist_path) { + File playlist_file; + + auto error = playlist_file.open(playlist_path.string()); + if (!error.is_valid()) { + std::string line; + char one_char[1]; + for (size_t pointer =0 ; pointer < playlist_file.size(); pointer++) { + playlist_file.seek(pointer); + playlist_file.read(one_char, 1); + if ((int) one_char[0] >= ' ') { + line += one_char[0]; + } else if (one_char[0] == '\n') { + txtline_process(line); + line.clear(); + } + } + if (line.length() > 0) { + txtline_process(line); + } + } + playlist_masterdb = playlist_db; + return ; + } + + + void PlaylistView::txtline_process(std::string &line) { + playlist_entry new_item; + rf::Frequency f; + size_t previous = 0; + size_t current = line.find(','); + std::string freqs = line.substr(0, current); + previous = current +1; + current = line.find(',', previous); + std::string file = line.substr(previous, current - previous); + previous = current +1; + current = line.find(',', previous); + uint32_t sample_rate = strtoll(line.substr(previous).c_str(), nullptr, 10); + + f = strtoll(freqs.c_str(), nullptr, 0); + new_item.replay_frequency = f; + new_item.replay_file = "/" + file; + new_item.sample_rate = sample_rate; + new_item.next_delay = 0; + + playlist_db.push_back(std::move(new_item)); + } + +void PlaylistView::on_file_changed(std::filesystem::path new_file_path, rf::Frequency replay_frequency, uint32_t replay_sample_rate) { + File data_file; + // Get file size + auto data_open_error = data_file.open("/" + new_file_path.string()); + if (data_open_error.is_valid()) { + file_error(); + return; + } + + file_path = new_file_path; + field_frequency.set_value(replay_frequency); + + sample_rate = replay_sample_rate; + + text_sample_rate.set(unit_auto_scale(sample_rate, 3, 0) + "Hz"); + + auto file_size = data_file.size(); + auto duration = (file_size * 1000) / (2 * 2 * sample_rate); + + progressbar.set_max(file_size); + text_filename.set(file_path.filename().string().substr(0, 12)); + text_duration.set(to_string_time_ms(duration)); + + button_play.focus(); +} + +void PlaylistView::on_tx_progress(const uint32_t progress) { + progressbar.set_value(progress); +} + +void PlaylistView::focus() { + button_open.focus(); +} + +void PlaylistView::file_error() { + nav_.display_modal("Error", "File "+file_path.string() +" read error. " +file_path.string()); +} + +bool PlaylistView::is_active() const { + return (bool)replay_thread; +} + +bool PlaylistView::loop() const { + return (bool) playlist_db.size(); +} + +void PlaylistView::toggle() { + if( is_active() ) { + stop(false); + } else { + + start(); + } +} + +void PlaylistView::start() { + stop(false); + + playlist_entry item = playlist_db.front(); + playlist_db.pop_front(); + // playlist_entry item = playlist_db[0]; +// for (playlist_entry item : playlist_db) { + // file_path = item.replay_file; + // rf::Frequency replay_frequency = strtoll(item.replay_frequency.c_str(),nullptr,10); + on_file_changed(item.replay_file, item.replay_frequency, item.sample_rate); + on_target_frequency_changed(item.replay_frequency); + + std::unique_ptr reader; + + auto p = std::make_unique(); + auto open_error = p->open(file_path); + if( open_error.is_valid() ) { + file_error(); + return; // Fixes TX bug if there's a file error + } else { + reader = std::move(p); + } + + if( reader ) { + button_play.set_bitmap(&bitmap_stop); + baseband::set_sample_rate(sample_rate * 8); + + replay_thread = std::make_unique( + std::move(reader), + read_size, buffer_count, + &ready_signal, + [](uint32_t return_code) { + ReplayThreadDoneMessage message { return_code }; + EventDispatcher::send_message(message); + } + ); + } + field_rfgain.on_change = [this](int32_t v) { + tx_gain = v; + }; + field_rfgain.set_value(tx_gain); + receiver_model.set_tx_gain(tx_gain); + + + field_rfamp.on_change = [this](int32_t v) { + rf_amp = (bool)v; + }; + field_rfamp.set_value(rf_amp ? 14 : 0); + + //Enable Bias Tee if selected + radio::set_antenna_bias(portapack::get_antenna_bias()); + + radio::enable({ + receiver_model.tuning_frequency(), + sample_rate * 8, + baseband_bandwidth, + rf::Direction::Transmit, + rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps + static_cast(receiver_model.lna()), + static_cast(receiver_model.vga()) + }); + +// } +} + +void PlaylistView::stop(const bool do_loop) { + if( is_active() ) { + replay_thread.reset(); + } + if (do_loop) { + if (playlist_db.size() > 0 ) { + start(); + } else { + playlist_db = playlist_masterdb; + start(); + } + } else { + radio::set_antenna_bias(false); //Turn off Bias Tee + radio::disable(); + button_play.set_bitmap(&bitmap_play); + } + + ready_signal = false; +} + +void PlaylistView::handle_replay_thread_done(const uint32_t return_code) { + if (return_code == ReplayThread::END_OF_FILE) { + stop(true); + } else if (return_code == ReplayThread::READ_ERROR) { + stop(false); + file_error(); + } + + progressbar.set_value(0); +} + +PlaylistView::PlaylistView( + NavigationView& nav +) : nav_ (nav) +{ + + tx_gain = 35;field_rfgain.set_value(tx_gain); // Initial default value (-12 dB's max ). + field_rfamp.set_value(rf_amp ? 14 : 0); // Initial default value True. (TX RF amp on , +14dB's) + + baseband::run_image(portapack::spi_flash::image_tag_replay); + + add_children({ + &labels, + &button_open, + &text_filename, + &text_sample_rate, + &text_duration, + &progressbar, + &field_frequency, + &field_rfgain, + &field_rfamp, // let's not use common rf_amp + &check_loop, + &button_play, + &waterfall, + }); + + field_frequency.set_value(target_frequency()); + field_frequency.set_step(receiver_model.frequency_step()); + field_frequency.on_change = [this](rf::Frequency f) { + this->on_target_frequency_changed(f); + }; + field_frequency.on_edit = [this, &nav]() { + // TODO: Provide separate modal method/scheme? + auto new_view = nav.push(this->target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + this->on_target_frequency_changed(f); + this->field_frequency.set_value(f); + }; + }; + + field_frequency.set_step(5000); + + button_play.on_select = [this](ImageButton&) { + this->toggle(); + }; + + button_open.on_select = [this, &nav](Button&) { + auto open_view = nav.push(".TXT"); + open_view->on_changed = [this](std::filesystem::path new_file_path) { + load_file(new_file_path); + }; + }; +} + +PlaylistView::~PlaylistView() { + radio::disable(); + baseband::shutdown(); +} + +void PlaylistView::on_hide() { + stop(false); + // TODO: Terrible kludge because widget system doesn't notify Waterfall that + // it's being shown or hidden. + waterfall.on_hide(); + View::on_hide(); +} + +void PlaylistView::set_parent_rect(const Rect new_parent_rect) { + View::set_parent_rect(new_parent_rect); + + const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height }; + waterfall.set_parent_rect(waterfall_rect); +} + +void PlaylistView::on_target_frequency_changed(rf::Frequency f) { + set_target_frequency(f); +} + +void PlaylistView::set_target_frequency(const rf::Frequency new_value) { + persistent_memory::set_tuned_frequency(new_value);; +} + +rf::Frequency PlaylistView::target_frequency() const { + return persistent_memory::tuned_frequency(); +} + +} /* namespace ui */ diff --git a/firmware/application/apps/ui_playlist.hpp b/firmware/application/apps/ui_playlist.hpp new file mode 100644 index 00000000..228a0a50 --- /dev/null +++ b/firmware/application/apps/ui_playlist.hpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 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_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "replay_thread.hpp" +#include "ui_spectrum.hpp" + +#include +#include +#include + +namespace ui { + +class PlaylistView : public View { +public: + PlaylistView(NavigationView& nav); + ~PlaylistView(); + + void on_hide() override; + void set_parent_rect(const Rect new_parent_rect) override; + void focus() override; + + std::string title() const override { return "Playlist"; }; + +private: + NavigationView& nav_; + + static constexpr ui::Dim header_height = 3 * 16; + + struct playlist_entry { + rf::Frequency replay_frequency { 0 }; + std::string replay_file{}; + uint32_t sample_rate{}; + uint32_t next_delay{}; + }; + std::deque playlist_db{}; + std::deque playlist_masterdb{}; + uint32_t sample_rate = 0; + int32_t tx_gain { 47 }; + bool rf_amp { true }; // aux private var to store temporal, Replay App rf_amp user selection. + static constexpr uint32_t baseband_bandwidth = 2500000; + const size_t read_size { 16384 }; + const size_t buffer_count { 3 }; + void load_file(std::filesystem::path playlist_path); + void txtline_process(std::string &); + void on_file_changed(std::filesystem::path new_file_path, rf::Frequency replay_frequency, uint32_t replay_sample_rate); + void on_target_frequency_changed(rf::Frequency f); + void on_tx_progress(const uint32_t progress); + + void set_target_frequency(const rf::Frequency new_value); + rf::Frequency target_frequency() const; + + void toggle(); + void start(); + void stop(const bool do_loop); + bool is_active() const; + bool loop() const; + void set_ready(); + void handle_replay_thread_done(const uint32_t return_code); + void file_error(); + + std::filesystem::path file_path { }; + std::unique_ptr replay_thread { }; + bool ready_signal { false }; + + Labels labels { + { { 10 * 8, 2 * 16 }, "GAIN A:", Color::light_grey() } + }; + + Button button_open { + { 0 * 8, 0 * 16, 10 * 8, 2 * 16 }, + "Open file" + }; + + + Text text_filename { + { 11 * 8, 0 * 16, 12 * 8, 16 }, + "-" + }; + Text text_sample_rate { + { 24 * 8, 0 * 16, 6 * 8, 16 }, + "-" + }; + + Text text_duration { + { 11 * 8, 1 * 16, 6 * 8, 16 }, + "-" + }; + ProgressBar progressbar { + { 18 * 8, 1 * 16, 12 * 8, 16 } + }; + + FrequencyField field_frequency { + { 0 * 8, 2 * 16 }, + }; + + NumberField field_rfgain { + { 14 * 8, 2 * 16 }, + 2, + { 0, 47 }, + 1, + ' ' + }; + NumberField field_rfamp { // previously I was using "RFAmpField field_rf_amp" but that is general Receiver amp setting. + { 19 * 8, 2 * 16 }, + 2, + { 0, 14 }, // this time we will display GUI , 0 or 14 dBs same as Mic App + 14, + ' ' + }; + Checkbox check_loop { + { 21 * 8, 2 * 16 }, + 4, + "Loop", + true + }; + ImageButton button_play { + { 28 * 8, 2 * 16, 2 * 8, 1 * 16 }, + &bitmap_play, + Color::green(), + Color::black() + }; + + spectrum::WaterfallWidget waterfall { }; + + MessageHandlerRegistration message_handler_replay_thread_error { + Message::ID::ReplayThreadDone, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->handle_replay_thread_done(message.return_code); + } + }; + + MessageHandlerRegistration message_handler_fifo_signal { + Message::ID::RequestSignal, + [this](const Message* const p) { + const auto message = static_cast(p); + if (message->signal == RequestSignalMessage::Signal::FillRequest) { + this->set_ready(); + } + } + }; + + MessageHandlerRegistration message_handler_tx_progress { + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.progress); + } + }; +}; + +} /* namespace ui */ diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 9d310a62..65641d75 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -65,6 +65,7 @@ //#include "ui_test.hpp" #include "ui_tone_search.hpp" #include "ui_touchtunes.hpp" +#include "ui_playlist.hpp" #include "ui_view_wav.hpp" #include "ui_whipcalc.hpp" @@ -518,6 +519,8 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) { { "SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav](){ nav.push(); } }, { "TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav](){ nav.push(); } }, { "TouchTune", ui::Color::yellow(), &bitmap_icon_remote, [&nav](){ nav.push(); } }, + { "Playlist", ui::Color::yellow(), &bitmap_icon_remote, [&nav](){ nav.push(); } }, + //{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push(); } }, }); } @@ -536,9 +539,10 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) { //{ "Notepad", ui::Color::dark_grey(), &bitmap_icon_notepad, [&nav](){ nav.push(); } }, { "Signal gen", ui::Color::green(), &bitmap_icon_cwgen, [&nav](){ nav.push(); } }, //{ "Tone search", ui::Color::dark_grey(), nullptr, [&nav](){ nav.push(); } }, - { "WAV viewer", ui::Color::yellow(), &bitmap_icon_soundboard, [&nav](){ nav.push(); } }, + { "Wav viewer", ui::Color::yellow(), &bitmap_icon_soundboard, [&nav](){ nav.push(); } }, { "Antenna length", ui::Color::green(), &bitmap_icon_tools_antenna, [&nav](){ nav.push(); } }, - { "Wipe SD Card", ui::Color::red(), &bitmap_icon_tools_wipesd, [&nav](){ nav.push(); } }, + + { "Wipe SD card", ui::Color::red(), &bitmap_icon_tools_wipesd, [&nav](){ nav.push(); } }, }); set_max_rows(2); // allow wider buttons } diff --git a/sdcard/PLAYLIST.TXT b/sdcard/PLAYLIST.TXT new file mode 100644 index 00000000..8c5b671a --- /dev/null +++ b/sdcard/PLAYLIST.TXT @@ -0,0 +1,3 @@ +##FREQ FILE SAMPLE RATE +315000000,SAMPLES/TeslaChargePort_US.C16,500000 +433920000,SAMPLES/TeslaChargePort_EU_AU.C16,500000