diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 2da43a148..c2af860b2 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -357,7 +357,11 @@ set(EXTCPPSRC #two_tone_rx external/two_tone_rx/main.cpp - external/two_tone_rx/ui_two_tone_rx.cpp + external/two_tone_rx/ui_two_tone_rx.cpp + + #hard_reset + external/hard_reset/main.cpp + external/hard_reset/ui_hard_reset.cpp ) set(EXTAPPLIST @@ -446,6 +450,7 @@ set(EXTAPPLIST p25_tx two_tone_pager two_tone_rx + hard_reset ) # sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 958277397..b37a60518 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -109,6 +109,7 @@ MEMORY ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k + ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k } SECTIONS @@ -628,4 +629,10 @@ SECTIONS KEEP(*(.external_app.app_flex_tx.application_information)); *(*ui*external_app*flex_tx*); } > ram_external_app_flex_tx + + .external_app_hard_reset : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_hard_reset.application_information)); + *(*ui*external_app*hard_reset*); + } > ram_external_app_hard_reset } diff --git a/firmware/application/external/hard_reset/main.cpp b/firmware/application/external/hard_reset/main.cpp new file mode 100644 index 000000000..3728246a1 --- /dev/null +++ b/firmware/application/external/hard_reset/main.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 Pezsma + * + * 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.hpp" +#include "ui_hard_reset.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::hard_reset { + +void initialize_app(NavigationView& nav) { + nav.push(); +} + +} // namespace ui::external_app::hard_reset + +extern "C" { + +__attribute__((section(".external_app.app_hard_reset.application_information"), used)) +application_information_t _application_information_hard_reset = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::hard_reset::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Hard Reset", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0xC0, + 0x01, + 0xFF, + 0x7F, + 0xFF, + 0x7F, + 0x56, + 0x35, + 0x56, + 0x35, + 0x56, + 0x35, + 0x56, + 0x35, + 0x56, + 0x35, + 0x56, + 0x35, + 0x56, + 0x35, + 0x56, + 0x35, + 0xFE, + 0x3F, + 0xFE, + 0x3F, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::red().v, + /*.menu_location = */ app_location_t::SETTINGS, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {0, 0, 0, 0}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; + +} // extern "C" diff --git a/firmware/application/external/hard_reset/ui_hard_reset.cpp b/firmware/application/external/hard_reset/ui_hard_reset.cpp new file mode 100644 index 000000000..da302ce94 --- /dev/null +++ b/firmware/application/external/hard_reset/ui_hard_reset.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2026 Pezsma + * + * 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_hard_reset.hpp" + +#include "portapack.hpp" +#include "file.hpp" +#include "file_path.hpp" +#include "ui_touch_calibration.hpp" +#include "ui_external_items_menu_loader.hpp" +#include "app_settings.hpp" + +using namespace portapack; +namespace pmem = portapack::persistent_memory; + +namespace ui::external_app::hard_reset { +HardResetView::HardResetView(ui::NavigationView& nav) + : nav_(nav) { + add_children({&chk_settings_file, + &chk_bad_apps, + &chk_pmem, + &txt_settings_file_count, + &txt_bad_apps_count, + &btn_yes, + &btn_no, + &text, + &txt_wait}); + + txt_wait.set_style(Theme::getInstance()->warning_dark); + + btn_yes.on_select = [this](Button&) { + { // to free painter after draw + Painter painter; + txt_wait.hidden(false); + text.hidden(true); + painter.fill_rectangle(text.screen_rect(), this->style().background); + txt_wait.paint(painter); + } + if (chk_settings_file.value()) { + clear_settings_folder(); + } + if (chk_bad_apps.value()) { + delete_bad_apps(apps_dir); + } + if (chk_pmem.value()) { + pmem::cache::defaults(); + StatusRefreshMessage message{}; + EventDispatcher::send_message(message); + nav_.replace(); + } else { + nav_.pop(); + } + }; + + btn_no.on_select = [this](Button&) { + nav_.pop(); + }; + txt_settings_file_count.set_style(Theme::getInstance()->fg_magenta); + txt_bad_apps_count.set_style(Theme::getInstance()->fg_magenta); + + signal_token_tick_second = rtc_time::signal_tick_second += [this]() { + this->calculate(); + rtc_time::signal_tick_second -= signal_token_tick_second; + }; +} + +HardResetView::~HardResetView() { + rtc_time::signal_tick_second -= signal_token_tick_second; +} + +void HardResetView::calculate() { + settings_file_count = count_ini_files_in_directory(settings_dir); + txt_settings_file_count.set(to_string_dec_uint(settings_file_count)); + + bad_apps_count = count_bad_apps(apps_dir); + txt_bad_apps_count.set(to_string_dec_uint(bad_apps_count)); + + if (settings_file_count > 0) { + chk_settings_file.set_value(true); + } + if (bad_apps_count > 0) { + chk_bad_apps.set_value(true); + } + chk_pmem.set_value(true); + txt_wait.hidden(true); + text.set("Warning! Checked items will beerased (bad apps, P.Mem, settings). Uncheck to keep. Touch calibration is required only if P.Mem is reset."); +} + +void HardResetView::focus() { + btn_no.focus(); +} + +uint16_t HardResetView::count_ini_files_in_directory(const std::filesystem::path& dir_path) { + int count = 0; + for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) { + if (std::filesystem::is_regular_file(entry.status())) { + count++; + } + } + return count; +} + +void HardResetView::delete_ini_files_in_directory(const std::filesystem::path& dir_path) { + bool found_and_deleted; + do { + found_and_deleted = false; + for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) { + if (std::filesystem::is_regular_file(entry.status())) { + auto full_path = dir_path / entry.path().filename(); + delete_file(full_path); + found_and_deleted = true; + break; // iterator can become invalid if the dir is modified, so break and restart the loop after deletion + } + } + } while (found_and_deleted); +} + +void HardResetView::clear_settings_folder() { + delete_ini_files_in_directory(settings_dir); +} + +bool HardResetView::is_bad_app(const std::filesystem::path& file_path, bool is_ppma) { + File app; + if (app.open(file_path)) { + return true; + } + bool is_bad = false; + if (is_ppma) { + application_information_t app_info = {}; + auto readResult = app.read(&app_info, sizeof(application_information_t)); + if (!readResult || readResult.value() != sizeof(application_information_t)) { + is_bad = true; + } else if (app_info.header_version != CURRENT_HEADER_VERSION) { + is_bad = true; + } else if (VERSION_MD5 != app_info.app_version) { + is_bad = true; + } else { + // --- CHECKSUM VALIDATION --- + // Similar to run_external_app: read through the file and calculate the checksum + app.seek(0); + uint32_t checksum = 0; + uint8_t buffer[512]; // 512-byte buffer to conserve RAM + + while (true) { + auto res = app.read(buffer, sizeof(buffer)); + if (!res) break; + checksum += simple_checksum((uint32_t)buffer, res.value()); + if (res.value() < sizeof(buffer)) break; // End of file + } + + if (checksum != EXT_APP_EXPECTED_CHECKSUM) { + is_bad = true; // The file body is corrupted / truncated! + } + } + } else { + // PPMP validation + standalone_application_information_t app_info = {}; + auto readResult = app.read(&app_info, sizeof(standalone_application_information_t)); + if (!readResult || readResult.value() != sizeof(standalone_application_information_t)) { + is_bad = true; + } else if (app_info.header_version > CURRENT_STANDALONE_APPLICATION_API_VERSION) { + is_bad = true; + } + } + + app.close(); + return is_bad; +} + +uint16_t HardResetView::count_bad_apps(const std::filesystem::path& apps_dir) { + int bad_count = 0; + + // Check .ppma files + for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) { + auto file_path = apps_dir / entry.path(); + if (is_bad_app(file_path, true)) { + bad_count++; + } + } + + // Check .ppmp files + for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) { + auto file_path = apps_dir / entry.path(); + if (is_bad_app(file_path, false)) { + bad_count++; + } + } + + return bad_count; +} + +void HardResetView::delete_bad_apps(const std::filesystem::path& apps_dir) { + // Delete bad .ppma files + bool deleted_something; + do { + deleted_something = false; + for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) { + auto file_path = apps_dir / entry.path(); + if (is_bad_app(file_path, true)) { + delete_file(file_path); + deleted_something = true; + break; + } + } + } while (deleted_something); + + // Delete bad .ppmp files + do { + deleted_something = false; + for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) { + auto file_path = apps_dir / entry.path(); + if (is_bad_app(file_path, false)) { + delete_file(file_path); + deleted_something = true; + break; + } + } + } while (deleted_something); +} + +} // namespace ui::external_app::hard_reset \ No newline at end of file diff --git a/firmware/application/external/hard_reset/ui_hard_reset.hpp b/firmware/application/external/hard_reset/ui_hard_reset.hpp new file mode 100644 index 000000000..bbdd826b7 --- /dev/null +++ b/firmware/application/external/hard_reset/ui_hard_reset.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 Pezsma + * + * 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_HARD_RESET_H__ +#define __UI_HARD_RESET_H__ + +#include "ui_navigation.hpp" +#include "signal.hpp" +using namespace ui; + +namespace ui::external_app::hard_reset { + +class HardResetView : public ui::View { + public: + HardResetView(ui::NavigationView& nav); + ~HardResetView(); + + std::string title() const override { return "Hard Reset"; } + void focus() override; + + private: + ui::NavigationView& nav_; + + SignalToken signal_token_tick_second{}; + ui::Button btn_yes{{UI_POS_X(1), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Erase sel."}; + ui::Button btn_no{{UI_POS_X_RIGHT(11), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)}, "No"}; + ui::Text text{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(5)}, ""}; + Checkbox chk_settings_file{{UI_POS_X(1), UI_POS_Y(1)}, 14, "Settings file:", true}; + Checkbox chk_bad_apps{{UI_POS_X(1), UI_POS_Y(2.5)}, 9, "Bad apps:", true}; + Checkbox chk_pmem{{UI_POS_X(1), UI_POS_Y(4)}, 11, "P.mem reset", true}; + + Text txt_settings_file_count{{UI_POS_X(19), UI_POS_Y(1), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"}; + Text txt_bad_apps_count{{UI_POS_X(14), UI_POS_Y(2.5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"}; + + Text txt_wait{{UI_POS_X_CENTER(14), UI_POS_Y(8), UI_POS_WIDTH(14), UI_POS_HEIGHT(1)}, "Please wait..."}; + + uint16_t count_ini_files_in_directory(const std::filesystem::path& dir_path); + void delete_ini_files_in_directory(const std::filesystem::path& dir_path); + void clear_settings_folder(); + void calculate(); + bool is_bad_app(const std::filesystem::path& file_path, bool is_ppma); + uint16_t count_bad_apps(const std::filesystem::path& apps_dir); + void delete_bad_apps(const std::filesystem::path& apps_dir); + + uint16_t settings_file_count = 0; + uint16_t bad_apps_count = 0; +}; + +} // namespace ui::external_app::hard_reset + +#endif // __UI_HARD_RESET_H__