tanmatsu: move identity + prefs to the SD card; open-probe the prefs load chain

The P4's internal FFat 'locfd' has a broken FAT metadata layer: open/
read/write work but f_stat/exists()/size() return garbage - and WHICH
garbage shifts with the build. At -Og the exists()-gated identity and
NodePrefs loads worked by luck; the -Os switch flipped the lie and the
device booted with a fresh identity and default name, and profile
changes never survived a reboot (the loads failed, not the saves).
Chat history was unaffected because it already lives on SD_MMC.

- DataStore::useSdMmcStorage(): full-store adoption of the card
  (/meshcomod root, identity store included), mirroring the T-Deck's
  useSdStorage(). The old card-root contacts3/channels2/adv_blobs from
  the secondary-FS era are renamed under /meshcomod.
- Tanmatsu boot: one-time rescue of identity + prefs off FFat by OPEN
  and READ probing (never exists()/size() on that FS), then adopt the
  card. FFat remains the no-card fallback only.
- DataStore::loadPrefs() probes candidates by opening and reading a
  byte instead of exists() - truthful on the P4, identical elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kaj Schittecat
2026-07-09 13:00:54 +02:00
co-authored by Claude Fable 5
parent d20241627c
commit 76cd025bf5
3 changed files with 92 additions and 7 deletions
+49 -3
View File
@@ -5,6 +5,9 @@
#include <SD.h>
#include "helpers/esp32/WdtHeavyGuard.h" // suspend core-0 idle WDT during the (SPIFFS-GC-prone) contact write
#endif
#if defined(HAS_TANMATSU)
#include <SD_MMC.h>
#endif
#if defined(EXTRAFS) || defined(QSPIFLASH)
#define MAX_BLOBRECS 100
@@ -240,16 +243,29 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
}
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
if (_fs->exists(_rp("/new_prefs"))) {
// Probe by OPENING, never exists(): on the Tanmatsu's internal FFat the FAT
// metadata layer lies (f_stat garbage — the tile cache hit the same thing),
// and WHICH lie you get shifts with the build (the -Og to -Os switch turned
// "loaded fine by luck" into "no prefs found -> default name every boot").
// open()+read is truthful there, and identical in behavior everywhere else.
auto probe = [this](const char* nm) -> bool {
File f = openRead(nm);
if (!f) return false;
uint8_t b;
const bool ok = f.read(&b, 1) == 1; // size() lies on the P4's FFat too — actually read
f.close();
return ok;
};
if (probe("/new_prefs")) {
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
} else if (_fs->exists(_rp("/new_prefs.tmp"))) {
} else if (probe("/new_prefs.tmp")) {
// Main file gone but a staged copy exists: a reboot landed between the temp
// write and the swap (or the swap was torn). Recover from it — this is the
// "device booted with the default name once" failure mode.
MESH_DEBUG_PRINTLN("DataStore: /new_prefs missing, recovering from .tmp");
loadPrefsInt("/new_prefs.tmp", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // re-establish the main file
} else if (_fs->exists(_rp("/node_prefs"))) {
} else if (probe("/node_prefs")) {
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // save to new filename
_fs->remove(_rp("/node_prefs")); // remove old
@@ -818,3 +834,33 @@ bool DataStore::useSdStorage() {
return true;
}
#endif
#if defined(HAS_TANMATSU)
// Tanmatsu: same full-store adoption as useSdStorage(), but on the SD_MMC slot.
// The internal FFat 'locfd' has a broken FAT metadata layer (f_stat/exists lie —
// see the tile-cache notes), and the exists()-gated identity + prefs loads that
// "worked" at -Og read different garbage at -Os and came up empty: fresh node
// identity, default name, profile changes gone every reboot. The card's FAT
// metadata is truthful, so identity/prefs move there with everything else.
// The caller migrates any FFat-resident files first (open()-probed, never
// exists() on FFat).
bool DataStore::useSdMmcStorage() {
if (!SD_MMC.exists("/meshcomod")) SD_MMC.mkdir("/meshcomod");
if (!SD_MMC.exists("/meshcomod/bl")) SD_MMC.mkdir("/meshcomod/bl");
if (!SD_MMC.exists("/meshcomod/identity")) SD_MMC.mkdir("/meshcomod/identity");
// The old secondary-FS layout kept contacts/channels/blobs at the CARD ROOT;
// pull them under /meshcomod so the rooted store keeps reading them.
static const char* k_move[] = { "/contacts3", "/channels2", "/adv_blobs" };
for (const char* nm : k_move) {
char dst[40];
snprintf(dst, sizeof dst, "/meshcomod%s", nm);
if (SD_MMC.exists(nm) && !SD_MMC.exists(dst)) SD_MMC.rename(nm, dst);
}
strncpy(_root, "/meshcomod", sizeof(_root) - 1);
_root[sizeof(_root) - 1] = '\0';
_fs = &SD_MMC;
_fsExtra = nullptr;
identity_store.use(SD_MMC, "/meshcomod/identity");
return true;
}
#endif
+6
View File
@@ -42,6 +42,12 @@ public:
// contacts, channels, blobs). Call after the SD card is mounted. Returns false
// if the dir can't be created. No-op on non-ESP32.
bool useSdStorage();
#endif
#if defined(HAS_TANMATSU)
// Full-store adoption of the SD_MMC card (Tanmatsu): identity + prefs move off
// the broken-metadata internal FFat (its exists()/f_stat lie, which made the
// gated loads come up empty). Caller migrates FFat-resident files first.
bool useSdMmcStorage();
#endif
FILESYSTEM* getPrimaryFS() const { return _fs; }
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
+37 -4
View File
@@ -375,12 +375,45 @@ static void wadameshSetup() {
printf("[storage] persistence disabled this boot (DataStore opens fail gracefully)\n");
}
// The internal FFat 'locfd' on this P4 has a broken FAT metadata layer (see the tile-cache notes):
// it keeps rarely-written data (identity/prefs) but LOSES the frequently-rewritten contacts + chat on
// reboot. So route contacts/channels (and chat history, see uiDataFsReady) to the microSD card, which
// is reliable FAT. Identity/NodePrefs stay on FFat — they persist there and survive a card-less boot.
// open/read/write work but f_stat/exists()/size() return garbage — and WHICH garbage shifts with
// the build (-Og "worked by luck"; -Os made the exists()-gated identity + prefs loads come up
// empty: fresh node identity, default name, profile changes lost every reboot). The card's FAT
// metadata is truthful, so the WHOLE store lives there now; FFat remains only the no-card fallback.
g_sd_ok = SD_MMC.begin("/sdcard", false /*1-bit*/) && SD_MMC.cardType() != CARD_NONE;
printf("[storage] SD_MMC.begin = %s\n", g_sd_ok ? "OK" : "no card");
if (g_sd_ok) store.setSecondaryFS(&SD_MMC); // contacts + channels -> SD; identity/prefs stay on FFat
if (g_sd_ok) {
SD_MMC.mkdir("/meshcomod");
SD_MMC.mkdir("/meshcomod/identity");
SD_MMC.mkdir("/meshcomod/bl");
if (g_fs_ok) {
// One-time rescue of identity + prefs off FFat: probe by OPEN + READ (never
// exists()/size() on this FS). Files the card already holds win — SD metadata
// is honest, so exists() is safe THERE.
static const char* k_mig[] = { "/new_prefs", "/new_prefs.tmp", "/node_prefs",
"/identity/_main.id" };
for (const char* nm : k_mig) {
char dst[48];
snprintf(dst, sizeof dst, "/meshcomod%s", nm);
if (SD_MMC.exists(dst)) continue;
File s = FFat.open(nm, FILE_READ);
if (!s) continue;
uint8_t buf[512];
size_t n = s.read(buf, sizeof buf);
if (n == 0) { s.close(); continue; } // ghost/empty entry — nothing to keep
File d = SD_MMC.open(dst, FILE_WRITE);
if (!d) { s.close(); continue; }
size_t total = 0;
do { d.write(buf, n); total += n; n = s.read(buf, sizeof buf); } while (n > 0);
d.close();
s.close();
printf("[storage] migrated %s -> SD:/meshcomod (%u B)\n", nm, (unsigned)total);
}
}
store.useSdMmcStorage(); // identity/prefs/contacts/channels all on the card
}
// No card: FFat stays the store, as before — identity/prefs loads there depend
// on the broken metadata layer and may be unreliable, but it is the only
// persistent option on a card-less unit.
store.begin();
bootLog("Mesh stack");