From 906ef08b050a1593bffe9d3df90997951d664b83 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:05:21 +0200 Subject: [PATCH] 3 way FS self-heal --- .../adapters/datastore/ZephyrDataStore.cpp | 98 +++++++++++++++++++ zephcore/adapters/datastore/ZephyrDataStore.h | 8 ++ zephcore/src/main_companion.cpp | 38 +++++++ 3 files changed, 144 insertions(+) diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 7a1b8de..d09fe87 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -362,6 +362,81 @@ void ZephyrDataStore::factoryReset() } } +/* ── First-boot migration ──────────────────────────────────────────── */ + +/* Marker written after the first clean ZephCore boot to prevent + * repeated auto-format on subsequent boots. */ +static constexpr const char *ZC_INIT_MARKER = "/lfs/_zc_init"; + +bool ZephyrDataStore::hasInitMarker() const +{ + return exists(ZC_INIT_MARKER); +} + +void ZephyrDataStore::writeInitMarker() +{ + struct fs_file_t f; + fs_file_t_init(&f); + if (fs_open(&f, ZC_INIT_MARKER, FS_O_CREATE | FS_O_WRITE) == 0) { + fs_close(&f); + } +} + +bool ZephyrDataStore::hasPrefs() const +{ + return exists(PREFS_FILE); +} + +/* Erase only the NVS (BLE bonds) partition — used when upgrading from + * firmware that had the storage_partition region as app code. That leaves + * bytes at 0xD0000 that can accidentally pass Zephyr NVS sector validation, + * causing settings_load() to hang and blocking bt_enable(). */ +void ZephyrDataStore::formatNVSOnly() +{ +#if FIXED_PARTITION_EXISTS(storage_partition) + const struct flash_area *fap; + int rc = flash_area_open(PARTITION_ID(storage_partition), &fap); + if (rc == 0) { + LOG_INF("formatNVSOnly: erasing NVS storage (%u bytes)", (unsigned)fap->fa_size); + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } else { + LOG_WRN("formatNVSOnly: flash_area_open(storage_partition) failed: %d", rc); + } +#else + LOG_DBG("formatNVSOnly: no storage_partition on this platform, skipped"); +#endif +} + +/* Returns true if the prefs file was written by Arduino MeshCore. + * Arduino's layout omits node_lat/node_lon (16 bytes inserted by ZephCore + * after node_name at offset 36), so freq/sf/bw land at the wrong offsets + * and produce values outside the physical RF ranges used as the signal. */ +bool ZephyrDataStore::prefsLookLikeArduino() const +{ + uint8_t buf[72]; + size_t len = 0; + if (!openRead(PREFS_FILE, buf, sizeof(buf), len) || len < 68) { + return false; + } + float freq, bw; + uint8_t sf; + memcpy(&freq, &buf[56], sizeof(float)); + sf = buf[60]; + memcpy(&bw, &buf[64], sizeof(float)); + return (freq < 300.0f || freq > 960.0f || + sf < 5 || sf > 12 || + bw < 6.0f || bw > 510.0f); +} + +/* Returns true if the old file-based BLE bonds file exists. + * Pre-NVS ZephCore (≤1.16.1) stored bonds via CONFIG_SETTINGS_FILE at this + * path; ≥1.16.2 moved to NVS. Presence means 0xD0000 has old app code. */ +bool ZephyrDataStore::hasOldSettingsFile() const +{ + return exists("/lfs/settings"); +} + /* ── Identity ──────────────────────────────────────────────────────── */ bool ZephyrDataStore::loadMainIdentity(mesh::LocalIdentity &identity) @@ -388,6 +463,11 @@ bool ZephyrDataStore::saveMainIdentity(const mesh::LocalIdentity &identity) void ZephyrDataStore::loadPrefs(NodePrefs &prefs) { + /* Save caller's defaults — restored if the file contains invalid radio + * params (e.g. an Arduino MeshCore new_prefs whose layout diverges from + * ZephCore at the freq/sf/bw offsets due to the inserted lat/lon fields). */ + NodePrefs saved_defaults = prefs; + bool prefs_exists = exists(PREFS_FILE); if (!prefs_exists) { LOG_DBG("loadPrefs: no prefs file found, persisting defaults"); @@ -428,6 +508,24 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) prefs.manual_add_contacts = buf[off++]; memcpy(&prefs.bw, &buf[off], sizeof(float)); off += 4; + + /* Sanity-check core radio params before consuming the rest of the file. + * An Arduino MeshCore new_prefs is layout-incompatible: ZephCore inserts + * node_lat (8) + node_lon (8) after node_name, shifting freq/sf/bw by + * +16 bytes. The misread values are freq≈0, sf≤1, bw=garbage — all + * outside the physical RF ranges below. Revert to the caller's defaults + * so the radio starts on the correct channel and the user can pair via + * BLE and reconfigure. */ + if (prefs.freq < 300.0f || prefs.freq > 960.0f || + prefs.sf < 5 || prefs.sf > 12 || + prefs.bw < 6.0f || prefs.bw > 510.0f) { + LOG_WRN("loadPrefs: radio params out of range " + "(freq=%.1f sf=%d bw=%.1f) — ignoring prefs (incompatible format?)", + (double)prefs.freq, (int)prefs.sf, (double)prefs.bw); + prefs = saved_defaults; + return; + } + prefs.tx_power_dbm = buf[off++]; prefs.telemetry_mode_base = buf[off++]; prefs.telemetry_mode_loc = buf[off++]; diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index 75bcdbb..2ed18e0 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -49,6 +49,14 @@ public: bool hasExternalStorage() const { return _has_ext_fs; } uint32_t getExternalStorageKb() const; + /* First-boot migration helpers — see formatNVSOnly() in .cpp */ + bool hasInitMarker() const; + void writeInitMarker(); + void formatNVSOnly(); + bool hasPrefs() const; + bool prefsLookLikeArduino() const; + bool hasOldSettingsFile() const; + static bool mount(); static void unmount(); static const char *mountPoint() { return MNT_POINT; } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 444138d..b5b9af2 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -939,6 +939,43 @@ int main(void) } data_store.begin(); + /* First-boot migration: fix NVS (BLE bonds) before bt_enable() runs. + * + * nRF52 stores BLE bonds in storage_partition (NVS) at 0xD0000. UF2 + * flashing only writes pages covered by the binary, leaving whatever was + * there before. Old firmware (Arduino MeshCore, ZephCore ≤1.16.1) used + * that region as app code; if those bytes accidentally pass Zephyr NVS + * sector validation, settings_load() hangs and BLE never advertises. + * + * A marker file /lfs/_zc_init is written after the first clean boot. + * If absent, we are on the first run of this ZephCore build: + * + * • No prefs, or Arduino prefs (layout-incompatible): full format. + * Covers fresh installs and Arduino MeshCore migrations. + * + * • Valid ZephCore prefs + /lfs/settings present: NVS erase only. + * The old file-based bonds backend (ZephCore ≤1.16.1) left this file; + * 0xD0000 is old app code → must erase. Identity/prefs/contacts + * are preserved; re-pair required (bonds were in /lfs/settings which + * the NVS backend ignores anyway). + * + * • Valid ZephCore prefs + no /lfs/settings: NVS was already initialized + * by ZephCore ≥1.16.2 — skip format entirely, bonds survive. */ + if (!data_store.hasInitMarker()) { + if (!data_store.hasPrefs() || data_store.prefsLookLikeArduino()) { + LOG_WRN("First ZephCore boot (%s) — formatting LFS + NVS", + data_store.hasPrefs() ? "Arduino prefs" : "no prefs"); + data_store.formatFileSystem(); + data_store.begin(); + } else if (data_store.hasOldSettingsFile()) { + LOG_WRN("Pre-NVS ZephCore upgrade (found /lfs/settings) — erasing NVS"); + data_store.formatNVSOnly(); + } else { + LOG_INF("ZephCore upgrade with valid NVS — skipping format, bonds preserved"); + } + data_store.writeInitMarker(); + } + /* Initialize sensor manager (GPS, environment sensors) */ sensor_manager_init(); @@ -991,6 +1028,7 @@ int main(void) companion_mesh.prefs.apc_enabled = 0; /* Default: APC off */ companion_mesh.prefs.apc_margin = 20; /* Companions: more conservative margin (mobile) */ companion_mesh.prefs.auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS; /* low-batt cutoff (0=off) */ + companion_mesh.prefs.gps_interval = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; /* 5-min duty cycle (0=always-on) */ /* Load prefs from storage */ data_store.loadPrefs(companion_mesh.prefs);