Add Backup & Reset sub-page with factory reset (developer feature)

Moves the previously standalone "Backup settings"/"Restore settings"
buttons out of the System menu into a new "Backup & Reset" sub-page
(replaced in the System menu by a single folder button, placed
directly above "Info"), and adds a third entry "Reset settings" - a
factory reset for developers/testers that deletes the entire
Flightradar folder from the SD card (settings, logbook, location
presets, WiFi, calibration - everything) and restarts the device. The
next boot picks up isFirstRun automatically (via the missing
SD_SETTINGS_FILE) and runs the full first-time setup again, exactly
like on the very first boot - no need to pull the SD card.

Since this is irreversible, it requires a full-screen red warning
confirmation first, reusing the same overlay previously built only for
the flight-logbook enable warning (confirmLogbookEnable() generalized
to confirmWarningScreen(), taking title/body StringIds as parameters).

New SdStorage::deleteDirectoryRecursive() and
SettingsBackup::factoryReset() implement the actual deletion + restart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-12 05:34:22 +02:00
parent 3c12b7bdfb
commit 9d2930bd8b
12 changed files with 219 additions and 26 deletions

View File

@@ -282,6 +282,21 @@ enum class StringId : uint8_t {
// Hoehen-Farbstufen erscheinen. // Hoehen-Farbstufen erscheinen.
LEGEND_GROUND_VEHICLE, LEGEND_GROUND_VEHICLE,
// Neue "Sicherung & Reset"-Unterseite im System-Menue (fasst die
// bisher einzeln im System-Menue stehenden Backup/Restore-Buttons
// zusammen und ergaenzt einen dritten Punkt "Einstellungen
// zuruecksetzen" - ein kompletter Werksreset fuer Entwickler/Tester,
// der den gesamten Flightradar-Ordner von der SD-Karte loescht und
// neu startet, siehe settings_backup.cpp::factoryReset()). Titel und
// Button-Label teilen sich MENU_BACKUP_RESET, genau wie
// MENU_CATEGORY_SYSTEM sowohl fuer den Menue-Button als auch den
// Bildschirmtitel der System-Seite verwendet wird.
MENU_BACKUP_RESET,
MENU_FACTORY_RESET,
MENU_FACTORY_RESET_WARNING_BODY,
MENU_FACTORY_RESET_DELETING,
MENU_FACTORY_RESET_FAILED,
COUNT COUNT
}; };

View File

@@ -231,6 +231,12 @@ static const char* const I18N_DE[] = {
"z.B. Zuhause, Arbeit, Ferienhaus", "z.B. Zuhause, Arbeit, Ferienhaus",
"Bodenfahrzeug", "Bodenfahrzeug",
"Sicherung & Reset",
"Einstellungen zurücksetzen",
"Löscht ALLE Daten von der SD-Karte (Einstellungen, Flugbuch, Standort-Presets, WLAN, Kalibrierung) und startet das Gerät neu. Danach läuft die komplette Ersteinrichtung erneut - wie beim allerersten Start. Das kann NICHT rückgängig gemacht werden!",
"Lösche alle Daten...",
"Zurücksetzen fehlgeschlagen.",
}; };
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,

View File

@@ -232,6 +232,12 @@ static const char* const I18N_EN[] = {
"e.g. Home, Work, Holiday home", "e.g. Home, Work, Holiday home",
"Ground vehicle", "Ground vehicle",
"Backup & Reset",
"Reset settings",
"Deletes ALL data from the SD card (settings, logbook, location presets, WiFi, calibration) and restarts the device. The full first-time setup will then run again, just like on the very first boot. This CANNOT be undone!",
"Deleting all data...",
"Reset failed.",
}; };
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,

View File

@@ -231,6 +231,12 @@ static const char* const I18N_ES[] = {
"ej. Casa, Trabajo, Casa de vacaciones", "ej. Casa, Trabajo, Casa de vacaciones",
"Vehículo terrestre", "Vehículo terrestre",
"Copia de seguridad y reinicio",
"Reiniciar ajustes",
"Elimina TODOS los datos de la tarjeta SD (ajustes, registro de vuelos, ubicaciones guardadas, WiFi, calibración) y reinicia el dispositivo. Después se ejecutará de nuevo la configuración inicial completa, como en el primer arranque. ¡Esto NO se puede deshacer!",
"Eliminando todos los datos...",
"Error al reiniciar.",
}; };
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,

View File

@@ -231,6 +231,12 @@ static const char* const I18N_FR[] = {
"ex. Maison, Travail, Maison de vacances", "ex. Maison, Travail, Maison de vacances",
"Véhicule au sol", "Véhicule au sol",
"Sauvegarde et réinitialisation",
"Réinitialiser réglages",
"Supprime TOUTES les données de la carte SD (réglages, journal, préréglages de position, WiFi, calibrage) et redémarre l'appareil. La configuration initiale complète s'exécutera alors à nouveau, comme au tout premier démarrage. Cette action est IRRÉVERSIBLE !",
"Suppression de toutes les données...",
"Échec de la réinitialisation.",
}; };
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,

View File

@@ -231,6 +231,12 @@ static const char* const I18N_IT[] = {
"es. Casa, Lavoro, Casa vacanze", "es. Casa, Lavoro, Casa vacanze",
"Veicolo a terra", "Veicolo a terra",
"Backup e reimpostazione",
"Reimposta impostazioni",
"Elimina TUTTI i dati dalla scheda SD (impostazioni, registro voli, posizioni preimpostate, WiFi, calibrazione) e riavvia il dispositivo. Verrà quindi eseguita di nuovo la configurazione iniziale completa, come al primo avvio. Questa azione NON può essere annullata!",
"Eliminazione di tutti i dati...",
"Reimpostazione fallita.",
}; };
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,

View File

@@ -231,6 +231,12 @@ static const char* const I18N_TR[] = {
"örn. Ev, İş, Tatil evi", "örn. Ev, İş, Tatil evi",
"Kara aracı", "Kara aracı",
"Yedekleme ve Sıfırlama",
"Ayarları sıfırla",
"SD karttaki TÜM verileri (ayarlar, uçuş kayıt defteri, konum ön ayarları, WiFi, kalibrasyon) siler ve cihazı yeniden başlatır. Ardından ilk çalıştırmadaki gibi kurulum işlemi tekrar başlar. Bu işlem GERİ ALINAMAZ!",
"Tüm veriler siliniyor...",
"Sıfırlama başarısız.",
}; };
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,

View File

@@ -80,7 +80,12 @@ namespace {
// Bildschirmflaeche errechnet, sodass die Buttons den Platz bis knapp // Bildschirmflaeche errechnet, sodass die Buttons den Platz bis knapp
// ueber den Bildschirmrand ausfuellen - andere Seiten bleiben bei ihren // ueber den Bildschirmrand ausfuellen - andere Seiten bleiben bei ihren
// eigenen, unveraenderten Zeilenhoehen. // eigenen, unveraenderten Zeilenhoehen.
constexpr uint8_t SYSTEM_ROW_COUNT = 10; // War 10, bevor "Sicherung"/"Wiederherstellen" in die neue
// "Sicherung & Reset"-Unterseite ausgelagert wurden (siehe
// BACKUP_RESET_ROW_COUNT unten) und dort durch einen einzelnen
// Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer
// Ordner-Button) = 9.
constexpr uint8_t SYSTEM_ROW_COUNT = 9;
constexpr int16_t SYSTEM_ROW_GAP = 4; constexpr int16_t SYSTEM_ROW_GAP = 4;
constexpr int16_t SYSTEM_START_Y = 18; constexpr int16_t SYSTEM_START_Y = 18;
constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10; constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10;
@@ -91,6 +96,21 @@ namespace {
(int16_t)(Config::SCREEN_WIDTH - 20), SYSTEM_ROW_H}; (int16_t)(Config::SCREEN_WIDTH - 20), SYSTEM_ROW_H};
} }
// Eigene Zeilenhoehe fuer die neue "Sicherung & Reset"-Unterseite (4
// Eintraege: Sichern/Wiederherstellen/Zuruecksetzen/Zurueck) - gleiches
// "aus verfuegbarem Platz errechnen"-Muster wie bei SYSTEM_ROW_H.
// ROW_START_Y wird mitbenutzt (siehe oben, gemeinsam mit rowRect()/
// flightRowRect()), nur GAP/COUNT/H sind eigene Werte.
constexpr uint8_t BACKUP_RESET_ROW_COUNT = 4;
constexpr int16_t BACKUP_RESET_ROW_GAP = 10;
constexpr int16_t BACKUP_RESET_END_Y = Config::SCREEN_HEIGHT - 10;
constexpr int16_t BACKUP_RESET_ROW_H =
(BACKUP_RESET_END_Y - ROW_START_Y - (BACKUP_RESET_ROW_COUNT - 1) * BACKUP_RESET_ROW_GAP) / BACKUP_RESET_ROW_COUNT;
Rect backupResetRowRect(uint8_t index) {
return {10, (int16_t)(ROW_START_Y + index * (BACKUP_RESET_ROW_H + BACKUP_RESET_ROW_GAP)),
(int16_t)(Config::SCREEN_WIDTH - 20), BACKUP_RESET_ROW_H};
}
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label, void drawButton(TFT_eSPI& tft, const Rect& r, const String& label,
bool active = false, bool danger = false) { bool active = false, bool danger = false) {
uint16_t accent = danger ? TFT_RED : TFT_GREEN; uint16_t accent = danger ? TFT_RED : TFT_GREEN;
@@ -161,16 +181,21 @@ namespace {
} }
// Warn-Ueberlage, die praktisch den kompletten Bildschirm einnimmt (nur // Warn-Ueberlage, die praktisch den kompletten Bildschirm einnimmt (nur
// ein paar Pixel Rand) und vor dem Einschalten des Flugbuchs erklaert, // ein paar Pixel Rand) - urspruenglich nur fuers Einschalten des
// warum es sich nach 24h automatisch wieder abschaltet. "Achtung!!!" // Flugbuchs gebaut (erklaert, warum es sich nach 24h automatisch
// steht ganz oben, mit einer Leerzeile Abstand zum Fliesstext darunter; // wieder abschaltet), jetzt generisch mit uebergebenen Titel-/Text-
// der Text scrollt bei Bedarf (laengere Uebersetzungen) ueber eigene // StringIds, damit sie auch fuer die "Einstellungen zuruecksetzen"-
// Pfeil-Buttons, OK/Zurueck bleiben dabei immer unten fix und // Bestaetigung (Werksreset) wiederverwendet werden kann - beides sind
// seltene, potenziell folgenreiche Aktionen, die dieselbe deutliche
// Warnung verdienen. "Achtung!!!" (titleId) steht ganz oben, mit einer
// Leerzeile Abstand zum Fliesstext (bodyId) darunter; der Text
// scrollt bei Bedarf (laengere Uebersetzungen) ueber eigene Pfeil-
// Buttons, OK/Zurueck bleiben dabei immer unten fix und
// kollisionsfrei sichtbar. Sternchen laufen im Hintergrund mit, wie auf // kollisionsfrei sichtbar. Sternchen laufen im Hintergrund mit, wie auf
// allen anderen Menue-Screens (nur der Radar-Screen selbst spart sich // allen anderen Menue-Screens (nur der Radar-Screen selbst spart sich
// das wegen der CPU-Last durch Abfragen/Zeichnen). Gibt true zurueck, // das wegen der CPU-Last durch Abfragen/Zeichnen). Gibt true zurueck,
// wenn "OK" angetippt wurde, false bei "Zurueck". // wenn "OK" angetippt wurde, false bei "Zurueck".
bool confirmLogbookEnable(TFT_eSPI& tft) { bool confirmWarningScreen(TFT_eSPI& tft, StringId titleId, StringId bodyId) {
constexpr int16_t BOX_X = 4; constexpr int16_t BOX_X = 4;
constexpr int16_t BOX_Y = 4; constexpr int16_t BOX_Y = 4;
constexpr int16_t BOX_W = Config::SCREEN_WIDTH - 2 * BOX_X; constexpr int16_t BOX_W = Config::SCREEN_WIDTH - 2 * BOX_X;
@@ -195,7 +220,7 @@ namespace {
constexpr int16_t VIEW_BOTTOM_NO_SCROLL = OK_Y - 8; constexpr int16_t VIEW_BOTTOM_NO_SCROLL = OK_Y - 8;
constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP; constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP;
String body = I18n::t(StringId::MENU_LOGBOOK_WARNING_BODY); String body = I18n::t(bodyId);
int16_t totalH = layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, 0, 0, 0, false); int16_t totalH = layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, 0, 0, 0, false);
bool scrollable = (totalH - VIEW_BOTTOM_NO_SCROLL) > 0; bool scrollable = (totalH - VIEW_BOTTOM_NO_SCROLL) > 0;
@@ -220,7 +245,7 @@ namespace {
tft.setTextDatum(MC_DATUM); tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_RED, TFT_BLACK); tft.setTextColor(TFT_RED, TFT_BLACK);
tft.setTextSize(2); tft.setTextSize(2);
tft.drawString(I18n::t(StringId::MENU_LOGBOOK_WARNING_TITLE), BOX_X + BOX_W / 2, TITLE_Y); tft.drawString(I18n::t(titleId), BOX_X + BOX_W / 2, TITLE_Y);
tft.setTextSize(1); tft.setTextSize(1);
tft.setTextDatum(TL_DATUM); tft.setTextDatum(TL_DATUM);
@@ -257,7 +282,7 @@ namespace {
} }
} }
enum class Page { Main, Region, System, Flight }; enum class Page { Main, Region, System, Flight, BackupReset };
} }
void run(TFT_eSPI& tft) { void run(TFT_eSPI& tft) {
@@ -342,14 +367,15 @@ void run(TFT_eSPI& tft) {
Rect brightnessBtn = systemRowRect(2); Rect brightnessBtn = systemRowRect(2);
Rect timeoutBtn = systemRowRect(3); Rect timeoutBtn = systemRowRect(3);
Rect nightDimBtn = systemRowRect(4); Rect nightDimBtn = systemRowRect(4);
Rect backupBtn = systemRowRect(5); Rect webuiBtn = systemRowRect(5);
Rect restoreBtn = systemRowRect(6); // "Sicherung & Reset" (Backup/Restore/Werksreset, siehe
Rect webuiBtn = systemRowRect(7); // Page::BackupReset unten) steht bewusst direkt ueber "Info",
// "Info" (About) steht bewusst als letzter Punkt direkt ueber dem // das seinerseits als letzter Punkt direkt ueber dem Zurueck-
// Zurueck-Button - so bleibt seine Position stabil, egal wie // Button steht - so bleiben beide Positionen stabil, egal wie
// viele weitere Punkte davor noch dazukommen. // viele weitere Punkte davor noch dazukommen.
Rect aboutBtn = systemRowRect(8); Rect backupResetBtn = systemRowRect(6);
Rect backBtn = systemRowRect(9); Rect aboutBtn = systemRowRect(7);
Rect backBtn = systemRowRect(8);
drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE)); drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE));
@@ -361,9 +387,8 @@ void run(TFT_eSPI& tft) {
drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent())); drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent()));
drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes())); drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes()));
drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled())); drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled()));
drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE));
drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI)); drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI));
drawButton(tft, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET));
drawButton(tft, aboutBtn, I18n::t(StringId::MENU_ABOUT)); drawButton(tft, aboutBtn, I18n::t(StringId::MENU_ABOUT));
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
@@ -388,7 +413,42 @@ void run(TFT_eSPI& tft) {
SettingsStore::setScreenTimeoutMinutes(next); SettingsStore::setScreenTimeoutMinutes(next);
} else if (nightDimBtn.contains(tap.x, tap.y)) { } else if (nightDimBtn.contains(tap.x, tap.y)) {
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled()); SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
} else if (backupBtn.contains(tap.x, tap.y)) { } else if (backupResetBtn.contains(tap.x, tap.y)) {
page = Page::BackupReset;
} else if (aboutBtn.contains(tap.x, tap.y)) {
AboutScreen::run(tft);
} else if (webuiBtn.contains(tap.x, tap.y)) {
WebUiScreen::run(tft);
} else if (backBtn.contains(tap.x, tap.y)) {
page = Page::Main;
}
} else if (page == Page::BackupReset) {
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_BACKUP_RESET));
Rect backupBtn = backupResetRowRect(0);
Rect restoreBtn = backupResetRowRect(1);
Rect resetBtn = backupResetRowRect(2);
Rect backBtn = backupResetRowRect(3);
drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE));
// Danger-Akzent (rot) - deutlich von Sichern/Wiederherstellen
// abgesetzt, da diese Aktion (nach Bestaetigung) ALLE Daten
// unwiderruflich loescht, siehe confirmWarningScreen() unten.
drawButton(tft, resetBtn, I18n::t(StringId::MENU_FACTORY_RESET), false, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
TouchInput::Point tap;
while (true) {
if (TouchInput::wasTapped(tap)) break;
MenuStars::update(tft);
delay(20);
}
if (backupBtn.contains(tap.x, tap.y)) {
bool ok = SettingsBackup::backup(); bool ok = SettingsBackup::backup();
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_BACKUP_SAVED : StringId::MENU_BACKUP_FAILED), showBriefMessage(tft, I18n::t(ok ? StringId::MENU_BACKUP_SAVED : StringId::MENU_BACKUP_FAILED),
ok ? TFT_GREEN : TFT_RED); ok ? TFT_GREEN : TFT_RED);
@@ -398,12 +458,26 @@ void run(TFT_eSPI& tft) {
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_RESTORED : StringId::MENU_RESTORE_FAILED), showBriefMessage(tft, I18n::t(ok ? StringId::MENU_RESTORED : StringId::MENU_RESTORE_FAILED),
ok ? TFT_GREEN : TFT_RED); ok ? TFT_GREEN : TFT_RED);
} }
} else if (aboutBtn.contains(tap.x, tap.y)) { } else if (resetBtn.contains(tap.x, tap.y)) {
AboutScreen::run(tft); if (confirmWarningScreen(tft, StringId::MENU_LOGBOOK_WARNING_TITLE,
} else if (webuiBtn.contains(tap.x, tap.y)) { StringId::MENU_FACTORY_RESET_WARNING_BODY)) {
WebUiScreen::run(tft); tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.drawString(I18n::t(StringId::MENU_FACTORY_RESET_DELETING),
Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2);
tft.setTextDatum(TL_DATUM);
// Erfolgsfall: factoryReset() startet das Geraet neu und
// kehrt nie zurueck - dieser Code danach laeuft nur im
// (seltenen) Fehlerfall (SD nicht eingehaengt) ueberhaupt
// weiter.
bool ok = SettingsBackup::factoryReset();
if (!ok) {
showBriefMessage(tft, I18n::t(StringId::MENU_FACTORY_RESET_FAILED), TFT_RED);
}
}
} else if (backBtn.contains(tap.x, tap.y)) { } else if (backBtn.contains(tap.x, tap.y)) {
page = Page::Main; page = Page::System;
} }
} else { // Page::Flight } else { // Page::Flight
@@ -486,7 +560,8 @@ void run(TFT_eSPI& tft) {
SettingsStore::setFlightLogbookEnabled(false); SettingsStore::setFlightLogbookEnabled(false);
SettingsStore::setFlightLogbookEnabledAtEpoch(0); SettingsStore::setFlightLogbookEnabledAtEpoch(0);
SettingsStore::setFlightLogbookSessionFile(""); SettingsStore::setFlightLogbookSessionFile("");
} else if (confirmLogbookEnable(tft)) { } else if (confirmWarningScreen(tft, StringId::MENU_LOGBOOK_WARNING_TITLE,
StringId::MENU_LOGBOOK_WARNING_BODY)) {
SettingsStore::setFlightLogbookEnabled(true); SettingsStore::setFlightLogbookEnabled(true);
SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)time(nullptr)); SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)time(nullptr));
// Leerer Eintrag erzwingt eine frische Sitzungsdatei beim // Leerer Eintrag erzwingt eine frische Sitzungsdatei beim

View File

@@ -143,4 +143,35 @@ void logEvent(const char* csvLine) {
f.close(); f.close();
} }
bool deleteDirectoryRecursive(const char* path) {
File dir = SD.open(path);
if (!dir || !dir.isDirectory()) {
if (dir) dir.close();
return false;
}
// Gleiches vorsichtiges "entry.name() koennte relativ ODER absolut
// sein"-Muster wie in flight_logbook.cpp::resetAllData() - anders als
// dort aber mit echter Rekursion in Unterordner (statt sie zu
// ueberspringen), da der Flightradar-Ordner welche enthaelt (logs/,
// screenshots/).
File entry = dir.openNextFile();
while (entry) {
bool isDir = entry.isDirectory();
String name = String(entry.name());
entry.close();
String fullPath = name.startsWith("/") ? name : String(path) + "/" + name;
if (isDir) {
deleteDirectoryRecursive(fullPath.c_str());
} else {
SD.remove(fullPath);
}
entry = dir.openNextFile();
}
dir.close();
return SD.rmdir(path);
}
} }

View File

@@ -8,4 +8,12 @@ namespace SdStorage {
void seedDefaultDataFiles(); void seedDefaultDataFiles();
void logEvent(const char* csvLine); void logEvent(const char* csvLine);
// Loescht rekursiv einen kompletten Ordner (alle Dateien und
// Unterordner) inklusive des Ordners selbst. Fuer den Menuepunkt
// "Einstellungen zuruecksetzen" (settings_backup.cpp::factoryReset()) -
// der einzige aktuelle Aufrufer, der damit den kompletten
// Flightradar-Ordner von der SD-Karte entfernt. Gibt false zurueck,
// wenn der Pfad nicht existiert/kein Ordner ist.
bool deleteDirectoryRecursive(const char* path);
} }

View File

@@ -57,4 +57,24 @@ bool hasBackup() {
return SD.exists(SETTINGS_BACKUP_FILE) || SD.exists(WIFI_BACKUP_FILE); return SD.exists(SETTINGS_BACKUP_FILE) || SD.exists(WIFI_BACKUP_FILE);
} }
bool factoryReset() {
if (!SdStorage::isMounted()) return false;
bool ok;
{
// Guard-Block bewusst vor dem Neustart wieder verlassen (statt
// ueber die Funktion hinweg zu halten) - reine Vorsicht, auch wenn
// ESP.restart() den Chip ohnehin sofort zuruecksetzt.
SdMutex::Guard guard;
ok = SdStorage::deleteDirectoryRecursive(Config::SD_ROOT_DIR);
}
if (ok) {
delay(200);
ESP.restart();
// Wird nie erreicht - ESP.restart() kehrt nicht zurueck.
}
return ok;
}
} }

View File

@@ -5,4 +5,12 @@ namespace SettingsBackup {
bool backup(); bool backup();
bool restore(); bool restore();
bool hasBackup(); bool hasBackup();
// Loescht den kompletten Flightradar-Ordner von der SD-Karte und
// startet das Geraet neu - beim naechsten Boot laeuft dadurch wieder
// die komplette Ersteinrichtung (siehe main.cpp: isFirstRun-Erkennung
// ueber Config::SD_SETTINGS_FILE). Kehrt nur im Fehlerfall zurueck
// (z.B. SD nicht eingehaengt) - im Erfolgsfall startet das Geraet
// neu, bevor die Funktion "zurueckkehrt" (ESP.restart()).
bool factoryReset();
} }