diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 6d2ac2afd..03eb99fb6 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -106,7 +106,10 @@ set(EXTCPPSRC #acars external/acars_rx/main.cpp external/acars_rx/acars_app.cpp - + + #shoppingcart_lock + external/shoppingcart_lock/main.cpp + external/shoppingcart_lock/shoppingcart_lock.cpp ) set(EXTAPPLIST @@ -135,4 +138,5 @@ set(EXTAPPLIST sstvtx random_password #acars_rx + shoppingcart_lock ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index d3cb0f8a9..1352b487e 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -48,6 +48,7 @@ MEMORY ram_external_app_sstvtx(rwx) : org = 0xADC70000, len = 32k ram_external_app_random_password(rwx) : org = 0xADC80000, len = 32k ram_external_app_acars_rx(rwx) : org = 0xADC90000, len = 32k + ram_external_app_shoppingcart_lock(rwx) : org = 0xADCA0000, len = 32k } SECTIONS @@ -166,7 +167,6 @@ SECTIONS *(*ui*external_app*tpmsrx*); } > ram_external_app_tpmsrx - .external_app_protoview : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_protoview.application_information)); @@ -204,6 +204,11 @@ SECTIONS *(*ui*external_app*acars_rx*); } > ram_external_app_acars_rx + .external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_shoppingcart_lock.application_information)); + *(*ui*external_app*shoppingcart_lock*); + } > ram_external_app_shoppingcart_lock diff --git a/firmware/application/external/shoppingcart_lock/main.cpp b/firmware/application/external/shoppingcart_lock/main.cpp new file mode 100644 index 000000000..3dad8ecfe --- /dev/null +++ b/firmware/application/external/shoppingcart_lock/main.cpp @@ -0,0 +1,63 @@ + +// RocketGod's Shopping Cart Lock app +// https://betaskynet.com +#include "ui.hpp" +#include "shoppingcart_lock.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::shoppingcart_lock { +void initialize_app(NavigationView& nav) { + baseband::run_image(portapack::spi_flash::image_tag_audio_tx); + nav.push(); +} +} // namespace ui::external_app::shoppingcart_lock + +extern "C" { + +__attribute__((section(".external_app.app_shoppingcart_lock.application_information"), used)) application_information_t _application_information_shoppingcart_lock = { + (uint8_t*)0x00000000, + ui::external_app::shoppingcart_lock::initialize_app, + CURRENT_HEADER_VERSION, + VERSION_MD5, + "Cart Lock", + { + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x7E, + 0x7E, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x7E, + 0x81, + 0x81, + 0x7E, + 0x00, + 0x00, + 0x00, + 0x7E, + 0x81, + 0x81, + 0x81, + 0x81, + 0x7E, + 0x00, + }, + ui::Color::red().v, + app_location_t::UTILITIES, + {'P', 'A', 'T', 'X'}, + 0x00000000, +}; +} \ No newline at end of file diff --git a/firmware/application/external/shoppingcart_lock/shoppingcart_lock.cpp b/firmware/application/external/shoppingcart_lock/shoppingcart_lock.cpp new file mode 100644 index 000000000..22c42988b --- /dev/null +++ b/firmware/application/external/shoppingcart_lock/shoppingcart_lock.cpp @@ -0,0 +1,235 @@ +// RocketGod's Shopping Cart Lock app +// https://betaskynet.com +#include "shoppingcart_lock.hpp" + +using namespace portapack; + +namespace ui::external_app::shoppingcart_lock { + +void ShoppingCartLock::log_event(const std::string& message) { + static const size_t MAX_LOG_LINES = 50; + static std::vector message_history; + + message_history.push_back(message); + if (message_history.size() > MAX_LOG_LINES) { + message_history.erase(message_history.begin()); + menu_view.clear(); + for (const auto& msg : message_history) { + menu_view.add_item({msg, + ui::Theme::getInstance()->fg_green->foreground, + nullptr, + [](KeyEvent) {}}); + } + } else { + menu_view.add_item({message, + ui::Theme::getInstance()->fg_green->foreground, + nullptr, + [](KeyEvent) {}}); + } + + menu_view.set_highlighted(menu_view.item_count() - 1); +} + +bool ShoppingCartLock::is_active() const { + return (bool)replay_thread; +} + +void ShoppingCartLock::focus() { + menu_view.focus(); +} + +void ShoppingCartLock::stop() { + log_event(">>> STOP_SEQUENCE_START"); + if (is_active()) { + log_event("... Resetting Replay Thread"); + replay_thread.reset(); + } + log_event("... Stopping Audio Output"); + audio::output::stop(); + + log_event("... Resetting State Variables"); + transmitter_model.disable(); + ready_signal = false; + thread_sync_complete = false; + looping = false; + current_file = ""; + log_event("<<< STOP_SEQUENCE_COMPLETE"); +} + +std::string ShoppingCartLock::list_wav_files() { + log_event(">>> WAV_SCAN_START"); + auto reader = std::make_unique(); + bool found_lock = false; + bool found_unlock = false; + + for (const auto& entry : std::filesystem::directory_iterator(wav_dir, u"*")) { + if (std::filesystem::is_regular_file(entry.status())) { + auto filename = entry.path().filename().string(); + std::transform(filename.begin(), filename.end(), filename.begin(), ::tolower); + + if (filename == shoppingcart_lock_file || filename == shoppingcart_unlock_file) { + std::string file_path = (wav_dir / filename).string(); + if (reader->open(file_path)) { + if (filename == shoppingcart_lock_file) { + found_lock = true; + log_event("... Found: " + shoppingcart_lock_file); + log_event("Sample Rate: " + std::to_string(reader->sample_rate())); + log_event("Channels: " + std::to_string(reader->channels())); + log_event("Bits/Sample: " + std::to_string(reader->bits_per_sample())); + } + if (filename == shoppingcart_unlock_file) { + found_unlock = true; + log_event("... Found: " + shoppingcart_unlock_file); + log_event("Sample Rate: " + std::to_string(reader->sample_rate())); + log_event("Channels: " + std::to_string(reader->channels())); + log_event("Bits/Sample: " + std::to_string(reader->bits_per_sample())); + } + } + } + + if (found_lock && found_unlock) { + break; + } + } + } + + if (!found_lock || !found_unlock) { + log_event("!!! Missing Required Files:"); + if (!found_lock) log_event("!!! Missing: " + shoppingcart_lock_file); + if (!found_unlock) log_event("!!! Missing: " + shoppingcart_unlock_file); + menu_view.hidden(true); + text_empty.hidden(false); + } else { + log_event("... All Required Files Found"); + menu_view.hidden(false); + text_empty.hidden(true); + } + + log_event("<<< WAV_SCAN_COMPLETE"); + return found_lock && found_unlock ? "Required WAV files found" : "Missing required WAV files"; +} + +void ShoppingCartLock::wait_for_thread() { + uint32_t timeout = 100; + while (!ready_signal && timeout > 0) { + chThdYield(); + timeout--; + } +} + +void ShoppingCartLock::restart_playback() { + auto reader = std::make_unique(); + std::string file_path = (wav_dir / current_file).string(); + + if (!reader->open(file_path)) return; + + replay_thread = std::make_unique( + std::move(reader), + BUFFER_SIZE, + NUM_BUFFERS, + &ready_signal, + [](uint32_t return_code) { + ReplayThreadDoneMessage message{return_code}; + EventDispatcher::send_message(message); + }); + + log_event(">> SENDING <<"); + audio::output::start(); + transmitter_model.enable(); +} + +void ShoppingCartLock::play_audio(const std::string& filename, bool loop) { + auto reader = std::make_unique(); + stop(); + + std::string file_path = (wav_dir / filename).string(); + if (!reader->open(file_path)) { + nav_.display_modal("Error", "Cannot open " + filename); + return; + } + + const uint32_t wav_sample_rate = reader->sample_rate(); + const uint16_t wav_bits_per_sample = reader->bits_per_sample(); + + current_file = filename; + looping = loop; + + replay_thread = std::make_unique( + std::move(reader), + BUFFER_SIZE, + NUM_BUFFERS, + &ready_signal, + [](uint32_t return_code) { + ReplayThreadDoneMessage message{return_code}; + EventDispatcher::send_message(message); + }); + + wait_for_thread(); + + log_event("... Configuring Baseband"); + + const uint32_t bb_sample_rate = 1536000; + const uint32_t decimation = bb_sample_rate / wav_sample_rate; + + baseband::set_audiotx_config( + bb_sample_rate / decimation, + 0.0f, + 5.0f, + wav_bits_per_sample, + wav_bits_per_sample, + 0, + true, + false, + false, + false); + + baseband::set_sample_rate(wav_sample_rate); + + log_event("... Starting Audio Output"); + audio::output::start(); + log_event("... Setting Max Volume"); + audio::headphone::set_volume(audio::headphone::volume_range().max); + + transmitter_model.enable(); + + log_event(">>> Playback Started <<<"); +} + +ShoppingCartLock::ShoppingCartLock(NavigationView& nav) + : nav_{nav} { + add_children({&menu_view, + &text_empty, + &button_lock, + &button_unlock, + &button_stop}); + + button_lock.on_select = [this](Button&) { + if (is_active()) stop(); + log_event(">>> LOCK_SEQUENCE_START"); + play_audio(shoppingcart_lock_file, true); + }; + + button_unlock.on_select = [this](Button&) { + if (is_active()) stop(); + log_event(">>> UNLOCK_SEQUENCE_START"); + play_audio(shoppingcart_unlock_file, true); + }; + + button_stop.on_select = [this](Button&) { + log_event(">>> STOPPING AUDIO"); + stop(); + }; + + list_wav_files(); + + log_event("[+] INITIALIZATION COMPLETE"); + log_event("[+] PORTAPACK ARMED"); + log_event("[*] STATUS: READY"); +} + +ShoppingCartLock::~ShoppingCartLock() { + stop(); + baseband::shutdown(); +} + +} // namespace ui::external_app::shoppingcart_lock diff --git a/firmware/application/external/shoppingcart_lock/shoppingcart_lock.hpp b/firmware/application/external/shoppingcart_lock/shoppingcart_lock.hpp new file mode 100644 index 000000000..abea3ca74 --- /dev/null +++ b/firmware/application/external/shoppingcart_lock/shoppingcart_lock.hpp @@ -0,0 +1,102 @@ +// RocketGod's Shopping Cart Lock app +// https://betaskynet.com +#pragma once + +#include "ui_widget.hpp" +#include "ui_transmitter.hpp" +#include "replay_thread.hpp" +#include "baseband_api.hpp" +#include "io_wave.hpp" +#include "audio.hpp" +#include "portapack_shared_memory.hpp" +#include "ui_language.hpp" +#include "file_path.hpp" + +namespace ui::external_app::shoppingcart_lock { + +class ShoppingCartLock : public View { + public: + explicit ShoppingCartLock(NavigationView& nav); + ~ShoppingCartLock(); + + ShoppingCartLock(const ShoppingCartLock&) = delete; + ShoppingCartLock& operator=(const ShoppingCartLock&) = delete; + + std::string title() const override { return "Cart Lock"; }; + + void focus() override; + + private: + static constexpr size_t BUFFER_SIZE = 8192; + static constexpr size_t NUM_BUFFERS = 8; + const std::string shoppingcart_lock_file{"shopping_cart_lock.wav"}; + const std::string shoppingcart_unlock_file{"shopping_cart_unlock.wav"}; + + NavigationView& nav_; + std::unique_ptr replay_thread{}; + bool ready_signal{false}; + bool thread_sync_complete{false}; + bool looping{false}; + std::string current_file{}; + + struct WAVProperties { + uint32_t sample_rate; + uint16_t bits_per_sample; + size_t file_size; + }; + + void log_event(const std::string& message); + std::string list_wav_files(); + void handle_error(const std::string& message); + void play_audio(const std::string& filename, bool loop = false); + void stop(); + bool is_active() const; + void wait_for_thread(); + void restart_playback(); + + MenuView menu_view{ + {0, 0, 240, 150}, + true}; + + Text text_empty{ + {40, 70, 160, 16}, + "RocketGod was here"}; + + Button button_lock{ + {40, 165, 160, 35}, + LanguageHelper::currentMessages[LANG_LOCK]}; + + Button button_unlock{ + {40, 205, 160, 35}, + LanguageHelper::currentMessages[LANG_UNLOCK]}; + + Button button_stop{ + {40, 245, 160, 35}, + LanguageHelper::currentMessages[LANG_STOP]}; + + 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) { + ready_signal = true; + } + }}; + + MessageHandlerRegistration message_handler_replay_thread_done{ + Message::ID::ReplayThreadDone, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + if (message.return_code == ReplayThread::END_OF_FILE && looping) { + if (is_active()) { + chThdSleepMilliseconds(50); + restart_playback(); + } + } else { + thread_sync_complete = true; + stop(); + } + }}; +}; + +} // namespace ui::external_app::shoppingcart_lock diff --git a/firmware/common/ui_language.cpp b/firmware/common/ui_language.cpp index 31c6bf945..206fbe0b8 100644 --- a/firmware/common/ui_language.cpp +++ b/firmware/common/ui_language.cpp @@ -1,7 +1,7 @@ #include "ui_language.hpp" // use the exact position in this array! the enum's value is the identifier. Best to add to the end -const char* LanguageHelper::englishMessages[] = {"OK", "Cancel", "Error", "Modem setup", "Debug", "Log", "Done", "Start", "Stop", "Scan", "Clear", "Ready", "Data:", "Loop", "Reset", "Pause", "Resume", "Flood", "Show QR", "Save"}; +const char* LanguageHelper::englishMessages[] = {"OK", "Cancel", "Error", "Modem setup", "Debug", "Log", "Done", "Start", "Stop", "Scan", "Clear", "Ready", "Data:", "Loop", "Reset", "Pause", "Resume", "Flood", "Show QR", "Save", "Lock", "Unlock"}; // multi language support will changes (not in use for now) const char** LanguageHelper::currentMessages = englishMessages; diff --git a/firmware/common/ui_language.hpp b/firmware/common/ui_language.hpp index 1ae182218..78dd213e4 100644 --- a/firmware/common/ui_language.hpp +++ b/firmware/common/ui_language.hpp @@ -58,7 +58,9 @@ enum LangConsts { LANG_RESUME, LANG_FLOOD, LANG_SHOWQR, - LANG_SAVE + LANG_SAVE, + LANG_LOCK, + LANG_UNLOCK }; class LanguageHelper { diff --git a/sdcard/WAV/shopping_cart_lock.wav b/sdcard/WAV/shopping_cart_lock.wav new file mode 100644 index 000000000..2b3c5b73a Binary files /dev/null and b/sdcard/WAV/shopping_cart_lock.wav differ diff --git a/sdcard/WAV/shopping_cart_unlock.wav b/sdcard/WAV/shopping_cart_unlock.wav new file mode 100644 index 000000000..66cf4eabc Binary files /dev/null and b/sdcard/WAV/shopping_cart_unlock.wav differ