From c4e5610408f3fe167e70aaa79f4a1f9d9d7a0b0a Mon Sep 17 00:00:00 2001 From: DeFiDude <59237470+DeFiDude@users.noreply.github.com> Date: Mon, 4 May 2026 01:08:17 -0600 Subject: [PATCH] Harden flash config persistence --- src/config/UserConfig.cpp | 37 ++++++++++++++++----- src/main.cpp | 14 ++++++-- src/storage/FlashStore.cpp | 14 ++++++-- src/ui/screens/LvMessagesScreen.cpp | 31 +++++++++++++++++- src/ui/screens/LvSettingsScreen.cpp | 51 +++++++++++++++++++---------- src/ui/screens/LvSettingsScreen.h | 2 ++ 6 files changed, 117 insertions(+), 32 deletions(-) diff --git a/src/config/UserConfig.cpp b/src/config/UserConfig.cpp index 63c6699..3a11390 100644 --- a/src/config/UserConfig.cpp +++ b/src/config/UserConfig.cpp @@ -188,24 +188,43 @@ bool UserConfig::save(FlashStore& flash) { bool UserConfig::load(SDStore& sd, FlashStore& flash) { String json = flash.readString(PATH_USER_CONFIG); - if (json.isEmpty()) { - Serial.println("[CONFIG] No saved config, using defaults"); - return false; + bool flashOk = false; + if (!json.isEmpty()) { + flashOk = parseJson(json); + } else { + Serial.println("[CONFIG] No flash config found"); } - bool ok = parseJson(json); - - if (ok && _settings.sdStorageEnabled && sd.isReady()) { + if (flashOk && _settings.sdStorageEnabled && sd.isReady()) { String sdJson = sd.readString(SD_PATH_USER_CONFIG); if (!sdJson.isEmpty()) { Serial.println("[CONFIG] Loading opt-in SD config"); bool sdOk = parseJson(sdJson); - _settings.sdStorageEnabled = true; - return sdOk; + if (sdOk) { + _settings.sdStorageEnabled = true; + return true; + } + Serial.println("[CONFIG] SD config invalid, keeping flash config"); + return true; } } - return ok; + if (!flashOk && sd.isReady()) { + String sdJson = sd.readString(SD_PATH_USER_CONFIG); + if (!sdJson.isEmpty()) { + Serial.println("[CONFIG] Flash config unavailable, trying SD config"); + bool sdOk = parseJson(sdJson); + if (sdOk) { + _settings.sdStorageEnabled = true; + flash.writeString(PATH_USER_CONFIG, serializeToJson()); + return true; + } + Serial.println("[CONFIG] SD config invalid"); + } + } + + if (!flashOk) Serial.println("[CONFIG] No saved config, using defaults"); + return flashOk; } bool UserConfig::save(SDStore& sd, FlashStore& flash) { diff --git a/src/main.cpp b/src/main.cpp index 340ae0f..43a6b20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1024,7 +1024,12 @@ void setup() { lvTimezoneScreen.setDoneCallback([goHome](int tzIdx) { userConfig.settings().timezoneIdx = (uint8_t)tzIdx; userConfig.settings().timezoneSet = true; - userConfig.save(sdStore, flash); + bool saved = userConfig.save(sdStore, flash); + if (!saved) { + Serial.println("[BOOT] Timezone save failed; staying in setup"); + ui.lvStatusBar().showToast("Save failed; storage unavailable", 3000); + return; + } Serial.printf("[BOOT] Timezone set: %s (%s)\n", TIMEZONE_TABLE[tzIdx].label, TIMEZONE_TABLE[tzIdx].posixTZ); // Apply timezone immediately @@ -1057,7 +1062,12 @@ void setup() { finalName = "Ratspeak.org-" + dh.substring(0, 3); } userConfig.settings().displayName = finalName; - userConfig.save(sdStore, flash); + bool saved = userConfig.save(sdStore, flash); + if (!saved) { + Serial.println("[BOOT] Display name save failed; staying in setup"); + ui.lvStatusBar().showToast("Save failed; storage unavailable", 3000); + return; + } // Also save to active identity slot if (identityMgr.activeIndex() >= 0) { identityMgr.setDisplayName(identityMgr.activeIndex(), finalName); diff --git a/src/storage/FlashStore.cpp b/src/storage/FlashStore.cpp index 7a4f392..da63108 100644 --- a/src/storage/FlashStore.cpp +++ b/src/storage/FlashStore.cpp @@ -13,8 +13,18 @@ bool FlashStore::begin() { } } if (!mounted) { - Serial.println("[FLASH] LittleFS mount failed on all known labels!"); - return false; + Serial.println("[FLASH] LittleFS mount failed on all known labels; formatting data partition..."); + for (const char* label : labels) { + if (LittleFS.begin(true, "/littlefs", 10, label)) { + Serial.printf("[FLASH] LittleFS formatted and mounted on partition '%s'\n", label); + mounted = true; + break; + } + } + if (!mounted) { + Serial.println("[FLASH] LittleFS format/mount failed on all known labels!"); + return false; + } } _ready = true; diff --git a/src/ui/screens/LvMessagesScreen.cpp b/src/ui/screens/LvMessagesScreen.cpp index 8fd67aa..da240ac 100644 --- a/src/ui/screens/LvMessagesScreen.cpp +++ b/src/ui/screens/LvMessagesScreen.cpp @@ -50,6 +50,35 @@ std::string shortText(const std::string& text, size_t maxLen) { return text.substr(0, maxLen - 3) + "..."; } +std::string chatPreviewText(const std::string& text, size_t maxLen) { + if (text.empty()) return text; + + std::string firstLine; + firstLine.reserve(std::min(text.size(), maxLen)); + bool truncated = false; + for (char c : text) { + if (c == '\r' || c == '\n') { + truncated = true; + break; + } + firstLine += (c == '\t') ? ' ' : c; + } + + while (!firstLine.empty() && firstLine.back() == ' ') { + firstLine.pop_back(); + } + + if (firstLine.size() > maxLen) { + firstLine = shortText(firstLine, maxLen); + } else if (truncated && firstLine.size() + 3 <= maxLen) { + firstLine += "..."; + } else if (truncated && firstLine.size() > 3) { + firstLine = firstLine.substr(0, maxLen - 3) + "..."; + } + + return firstLine; +} + std::string displayNameForPeer(AnnounceManager* am, const std::string& peerHex) { if (am) { std::string peerName = am->lookupName(peerHex); @@ -178,7 +207,7 @@ void LvMessagesScreen::rebuildList() { auto* s = _lxmf->getConversationSummary(ci.peerHex); if (s) { ci.lastTs = s->lastTimestamp; - ci.preview = s->lastPreview; + ci.preview = chatPreviewText(s->lastPreview, 56); ci.lastIncoming = s->lastIncoming; ci.unreadCount = s->unreadCount; ci.totalCount = s->totalCount; diff --git a/src/ui/screens/LvSettingsScreen.cpp b/src/ui/screens/LvSettingsScreen.cpp index 08453b3..3bfd60d 100644 --- a/src/ui/screens/LvSettingsScreen.cpp +++ b/src/ui/screens/LvSettingsScreen.cpp @@ -173,21 +173,27 @@ void LvSettingsScreen::skipToNextEditable(int dir) { bool LvSettingsScreen::settingNeedsReboot(const SettingItem& item) const { if (!_cfg) return false; - auto& s = _cfg->settings(); + const auto& s = _cfg->settings(); if (labelEq(item.label, "WiFi Mode")) return s.wifiMode != _rebootSnap.wifiMode; if (labelEq(item.label, "Active WiFi")) return s.wifiSTASelected != _rebootSnap.wifiSTASelected; - if (isWiFiSSIDLabel(item.label) || isWiFiPasswordLabel(item.label)) return rebootSettingsChanged(); - if (labelEq(item.label, "WiFi Scan") || labelEq(item.label, "Forget WiFi")) return rebootSettingsChanged(); + if (isWiFiSSIDLabel(item.label) || isWiFiPasswordLabel(item.label)) return interfaceSettingsChanged(); + if (labelEq(item.label, "WiFi Scan") || labelEq(item.label, "Forget WiFi")) return interfaceSettingsChanged(); + if (labelEq(item.label, "TCP Relay") || labelEq(item.label, "Relay Host") || + labelEq(item.label, "Relay Port")) return tcpSettingsChanged(); if (labelEq(item.label, "LAN Discovery")) return s.autoIfaceEnabled != _rebootSnap.autoIfaceEnabled; - if (labelEq(item.label, "SD Message Store")) return s.sdStorageEnabled != _rebootSnap.sdStorageEnabled; + if (labelEq(item.label, "SD Message Store")) return storageSettingsChanged(); return false; } bool LvSettingsScreen::categoryNeedsReboot(int catIdx) const { if (catIdx < 0 || catIdx >= (int)_categories.size()) return false; - return (labelEq(_categories[catIdx].name, "Interfaces") - || labelEq(_categories[catIdx].name, "Storage & Maintenance")) - && rebootSettingsChanged(); + if (labelEq(_categories[catIdx].name, "Interfaces")) { + return interfaceSettingsChanged() || tcpSettingsChanged(); + } + if (labelEq(_categories[catIdx].name, "Storage & Maintenance")) { + return storageSettingsChanged(); + } + return false; } bool LvSettingsScreen::confirmableAction(const SettingItem& item) const { @@ -1894,12 +1900,15 @@ void LvSettingsScreen::snapshotRebootSettings() { } bool LvSettingsScreen::rebootSettingsChanged() const { + return interfaceSettingsChanged() || storageSettingsChanged() || tcpSettingsChanged(); +} + +bool LvSettingsScreen::interfaceSettingsChanged() const { if (!_cfg) return false; - auto& s = _cfg->settings(); + const auto& s = _cfg->settings(); if (s.wifiMode != _rebootSnap.wifiMode) return true; if (s.wifiSTASelected != _rebootSnap.wifiSTASelected) return true; if (s.autoIfaceEnabled != _rebootSnap.autoIfaceEnabled) return true; - if (s.sdStorageEnabled != _rebootSnap.sdStorageEnabled) return true; if (s.wifiSTANetworks.size() != _rebootSnap.wifiSTANetworks.size()) return true; for (size_t i = 0; i < s.wifiSTANetworks.size(); i++) { if (s.wifiSTANetworks[i].ssid != _rebootSnap.wifiSTANetworks[i].ssid) return true; @@ -1908,6 +1917,11 @@ bool LvSettingsScreen::rebootSettingsChanged() const { return false; } +bool LvSettingsScreen::storageSettingsChanged() const { + if (!_cfg) return false; + return _cfg->settings().sdStorageEnabled != _rebootSnap.sdStorageEnabled; +} + void LvSettingsScreen::snapshotTCPSettings() { if (!_cfg) return; auto& s = _cfg->settings(); @@ -1997,11 +2011,8 @@ void LvSettingsScreen::applyAndSave() { else if (_sd && _flash) { saved = _cfg->save(*_sd, *_flash); } else if (_flash) { saved = _cfg->save(*_flash); } - // Apply TCP changes live (stop old clients, create new ones, clear transient nodes) - if (tcpChanged) { - snapshotTCPSettings(); - if (_tcpChangeCb) _tcpChangeCb(); - } + // TCP relay changes are persisted only. Recreating clients live can race + // in-flight sockets/netif teardown on ESP32; reboot applies them cleanly. // Apply GPS toggle live (start/stop GPS UART) if (s.gpsTimeEnabled != _gpsSnapEnabled) { @@ -2014,14 +2025,18 @@ void LvSettingsScreen::applyAndSave() { _rebootNeeded = rebootSettingsChanged(); if (_ui) { - if (_rebootNeeded && !wasRebootNeeded) { - _ui->lvStatusBar().showToast("Interface changes saved; reboot to apply", 3000); + if (!saved) { + _ui->lvStatusBar().showToast("Save failed", 2000); + } else if (_rebootNeeded && !wasRebootNeeded) { + _ui->lvStatusBar().showToast( + tcpChanged ? "TCP relay saved; reboot to apply" : "Interface changes saved; reboot to apply", + 3000); } else if (!_rebootNeeded && wasRebootNeeded) { _ui->lvStatusBar().showToast("Pending reboot cleared", 1500); } else if (tcpChanged) { - _ui->lvStatusBar().showToast("TCP relay updated", 1200); + _ui->lvStatusBar().showToast("TCP relay saved; reboot to apply", 3000); } else { - _ui->lvStatusBar().showToast(saved ? "Saved" : "Applied", 800); + _ui->lvStatusBar().showToast("Saved", 800); } } } diff --git a/src/ui/screens/LvSettingsScreen.h b/src/ui/screens/LvSettingsScreen.h index 165a3d7..22dfb8b 100644 --- a/src/ui/screens/LvSettingsScreen.h +++ b/src/ui/screens/LvSettingsScreen.h @@ -194,6 +194,8 @@ private: }; RebootSnapshot _rebootSnap; void snapshotRebootSettings(); + bool interfaceSettingsChanged() const; + bool storageSettingsChanged() const; bool rebootSettingsChanged() const; // TCP change detection