Harden flash config persistence

This commit is contained in:
DeFiDude
2026-05-04 01:08:17 -06:00
parent 149bb08d1a
commit c4e5610408
6 changed files with 117 additions and 32 deletions
+28 -9
View File
@@ -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) {
+12 -2
View File
@@ -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);
+12 -2
View File
@@ -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;
+30 -1
View File
@@ -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;
+33 -18
View File
@@ -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);
}
}
}
+2
View File
@@ -194,6 +194,8 @@ private:
};
RebootSnapshot _rebootSnap;
void snapshotRebootSettings();
bool interfaceSettingsChanged() const;
bool storageSettingsChanged() const;
bool rebootSettingsChanged() const;
// TCP change detection