mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-16 23:24:42 +00:00
flash cleaning improvements
This commit is contained in:
@@ -1114,6 +1114,17 @@ Codes `0x80`–`0x90` (`PUSH_CODE_*` in `app/CompanionMesh.h`). Most used:
|
||||
| `/lfs/repeater/regions2` | Region map | Header + 164B × N entries |
|
||||
| `storage_partition` (NVS, 0xD0000 nRF52) | BLE bonds + Zephyr settings | NVS settings backend (≥1.16.2; old `/lfs/settings` file detected by self-heal) |
|
||||
|
||||
> **Roles are not interchangeable.** Each role formats the whole volume on its first boot if the
|
||||
> volume holds no data for that role: the companion checks `/lfs/new_prefs`
|
||||
> (`ZephyrDataStore::hasPrefs()`), the repeater/room-server/observer check `/lfs/repeater/prefs`
|
||||
> and `/lfs/repeater/_main.id` (`RepeaterDataStore::hasRoleData()`). So flashing a repeater over
|
||||
> a companion — or the reverse — erases the previous role's identity, prefs and contacts, plus
|
||||
> `storage_partition` and QSPI. Export your identity before switching roles. The roles' files
|
||||
> never overlap physically (one LittleFS volume, one allocator); the reason for the wipe is that
|
||||
> they share 128 KB and the other role's data crowds out writes. Repeater, room server and
|
||||
> observer share `/lfs/repeater/` and the same prefs layout, so switching among *those three*
|
||||
> preserves the identity.
|
||||
|
||||
### Preferences Binary Layouts
|
||||
|
||||
Two distinct field-by-field serializations (NOT raw struct dumps), both Arduino-compatible
|
||||
|
||||
@@ -127,7 +127,17 @@ Regions control which flood packets the repeater forwards. The region tree is hi
|
||||
| `log stop` | Disable packet logging |
|
||||
| `log erase` | Erase the log file |
|
||||
| `log` | *(USB only)* Dump the full log file to USB serial |
|
||||
| `erase` | *(USB only)* Format the entire filesystem |
|
||||
| `erase` | *(USB only)* Factory reset: erase the entire LittleFS volume, the BLE-bond NVS, and external QSPI flash, then reboot |
|
||||
|
||||
> **`erase` is a true factory reset.** It flattens `lfs_partition` (identity, prefs, ACL,
|
||||
> region map, logs), `storage_partition` (BLE bonds) and `qspi_storage_partition` where
|
||||
> present — not just the files under `/lfs/repeater/`. The node comes back with a new
|
||||
> identity and default prefs. Erasing the volume rather than unlinking files is what makes
|
||||
> it able to recover a volume another firmware has written into: on nRF52840 the Adafruit
|
||||
> core's filesystem (used by Arduino MeshCore and Meshtastic) sits at 0xED000, inside our
|
||||
> `lfs_partition`, and its format scribbles the top 7 blocks of our volume. Switching
|
||||
> between Arduino-core firmware and ZephCore on nRF52840 needs an erase in **both**
|
||||
> directions — `tools/formatter` or a full chip erase.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# ZephCore 1.17.4-zephcore
|
||||
|
||||
Storage housekeeping. The repeater's `erase` command never actually erased anything, a node flashed
|
||||
from another firmware could start out with somebody else's leftovers underneath it, and switching a
|
||||
node between companion and repeater firmware quietly let the two share the same 128 KB. All three are
|
||||
fixed, and the last one is now deliberate and loud rather than quiet.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Read the role-switching section before you flash a different role onto an existing node.** A
|
||||
> companion that gets repeater firmware — or the reverse — now erases itself on first boot. That is
|
||||
> intentional, but it is new. Export your identity first if you want to keep it.
|
||||
|
||||
> [!NOTE]
|
||||
> A normal upgrade is unaffected. Repeater to repeater, or companion to companion, keeps your
|
||||
> identity, settings, contacts and phone pairing exactly as before.
|
||||
|
||||
---
|
||||
|
||||
## `erase` now erases
|
||||
|
||||
On a repeater or room server, `erase` promised to format the entire filesystem. It did not. It deleted
|
||||
the handful of files it had put in its own folder and cleared the phone pairings, and left everything
|
||||
else exactly where it was.
|
||||
|
||||
Most of the time nobody noticed, because on a healthy node those files *are* everything that matters.
|
||||
It mattered when the node was not healthy — which is precisely when somebody reaches for `erase`. If
|
||||
anything had written into the storage area from outside, deleting our own files could not undo it, and
|
||||
the command reported success while the problem stayed.
|
||||
|
||||
`erase` now wipes the storage area itself, the phone-pairing store, and external flash where a board has
|
||||
it, then reboots. The node comes back with a new identity and default settings, exactly like a node out
|
||||
of the box.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **This is a genuine factory reset now, and it takes the identity with it.** Anyone who had your node
|
||||
> in their contacts will need to add it again, and an admin password, ACL and region map all go too.
|
||||
> That was always what the command claimed to do; it is now what it does.
|
||||
|
||||
The companion's `erase` already worked this way. Repeater, room server and observer share one
|
||||
implementation with it now, so there is one behaviour to remember instead of two.
|
||||
|
||||
---
|
||||
|
||||
## Switching a node between roles now wipes it
|
||||
|
||||
Companion firmware and repeater firmware kept their files in separate folders, and until now each left
|
||||
the other's alone. Flashing back and forth preserved both sets.
|
||||
|
||||
That sounds generous and was not. The two roles share a single 128 KB storage area. A companion that has
|
||||
collected a few hundred contacts and a full advert cache leaves noticeably less room for a repeater's
|
||||
region map and access list, and a write that no longer fits simply fails. The node is not corrupted —
|
||||
the two roles' files never sit on top of each other — it just runs out of space for reasons its owner
|
||||
cannot see, because half of what is stored belongs to firmware that is not running.
|
||||
|
||||
They are also two quite different kinds of node, and treating one machine as quietly holding both was
|
||||
never worth the space it cost.
|
||||
|
||||
From 1.17.4, each role checks on first boot whether the storage belongs to it, and formats everything if
|
||||
not. So a repeater flashed onto a former companion starts empty, and a companion flashed onto a former
|
||||
repeater starts empty.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Export your identity before switching roles.** The node's identity, settings, contacts, channels,
|
||||
> access list, region map and phone pairings all go. There is no undo and no warning prompt — the first
|
||||
> boot on the new firmware has already done it by the time you see anything.
|
||||
|
||||
> [!NOTE]
|
||||
> **Repeater, room server and observer still share.** Those three keep their files in the same place and
|
||||
> use the same settings layout, so moving between them keeps the node's identity and configuration. It
|
||||
> is the companion that is now separate.
|
||||
|
||||
---
|
||||
|
||||
## A node coming from other firmware starts clean
|
||||
|
||||
Flashing ZephCore onto hardware that was running something else is a normal thing to do, and it used to
|
||||
leave more behind than anyone expected.
|
||||
|
||||
Nothing about installing firmware erases storage. Dragging a UF2 file writes the program and nothing
|
||||
else, and each firmware only ever clears the piece of flash it believes is its own. On the nRF52840
|
||||
boards, Arduino MeshCore's storage sits inside the same region ZephCore uses, so the two overlap — and
|
||||
whichever one boots first tidies up its own corner and leaves the rest of the other's files sitting
|
||||
there. The result on one Seeed Solar Node was a repeater that came up looking perfectly healthy and
|
||||
silently refused to forward anything, because a single setting deep inside its configuration had been
|
||||
overwritten by bytes that belonged to a different firmware.
|
||||
|
||||
Each role now checks on first boot whether the storage is its own and, if not, clears the whole lot —
|
||||
the storage area, the phone-pairing store, and external flash — before writing anything. A node arriving
|
||||
from another firmware, or from a factory-fresh chip, starts from a known state instead of an inherited
|
||||
one.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Moving between Arduino MeshCore and ZephCore still needs an erase in both directions.** ZephCore now
|
||||
> cleans up on the way in, but it cannot clean up on the way out — going back to Arduino MeshCore leaves
|
||||
> ZephCore's files inside the area Arduino will use. Run the formatter UF2, or a full chip erase, when
|
||||
> you switch either way. This is not new advice; it is now written down.
|
||||
|
||||
---
|
||||
|
||||
## Also in this release
|
||||
|
||||
Nothing here changes how a node behaves.
|
||||
|
||||
- **A wasted erase on the companion.** Running `erase` from the companion's USB console formatted the
|
||||
storage, then formatted it a second time on the reboot that followed, because the marker saying "this
|
||||
node has been set up" went out with everything else. The pairing-based factory reset never had this
|
||||
problem. Both paths behave the same way now.
|
||||
@@ -629,6 +629,10 @@ target_sources(app PRIVATE
|
||||
helpers/buzzer_gate.c
|
||||
# QSPI bring-up probe. Self-stubs unless CONFIG_ZEPHCORE_QSPI_RDID_PROBE.
|
||||
helpers/qspi_probe.c
|
||||
# Factory format of every storage region. Compiled for all roles: the
|
||||
# companion builds ZephyrDataStore, the other three build
|
||||
# RepeaterDataStore, and both call into this.
|
||||
adapters/datastore/ZephyrFsFormat.c
|
||||
)
|
||||
|
||||
# Boot-time hardware-RTC auto-discovery (compact raw-I2C). Always compiled so
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include "ZephyrDataStore.h"
|
||||
#include "ZephyrFsFormat.h"
|
||||
#include <AdvertDataHelpers.h> // ADV_TYPE_NONE (transient/anon contacts)
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zephyr/fs/littlefs.h>
|
||||
@@ -371,84 +372,14 @@ void ZephyrDataStore::checkAdvBlobFile()
|
||||
|
||||
bool ZephyrDataStore::formatFileSystem()
|
||||
{
|
||||
LOG_INF("formatFileSystem: starting...");
|
||||
/* The erase/remount itself lives in ZephyrFsFormat.c so the repeater,
|
||||
* room server and observer — which build RepeaterDataStore and never
|
||||
* compile this file — get the identical implementation. */
|
||||
bool ext_mounted = false;
|
||||
bool mounted = zephcore_fs_format_all(&ext_mounted);
|
||||
|
||||
/* Properly unmount from Zephyr's VFS before erasing flash.
|
||||
* The old unmount() only cleared flags — Zephyr still held /lfs mounted,
|
||||
* so flash_area_flatten silently destroyed the on-flash superblock while
|
||||
* LittleFS considered itself active. Every subsequent file op then hit
|
||||
* the erased blocks and logged "Corrupted dir pair at {0x0, 0x1}".
|
||||
* FS_FSTAB_DECLARE_ENTRY exposes the non-static mount struct generated
|
||||
* from the DTS fstab; fs_mount() on a blank partition auto-formats
|
||||
* (littlefs_fs.c: lfs_mount fail → lfs_format → lfs_mount). */
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(lfs));
|
||||
fs_unmount(&FS_FSTAB_ENTRY(DT_NODELABEL(lfs)));
|
||||
lfs_mounted = false;
|
||||
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs))
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(qspi_lfs));
|
||||
fs_unmount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs)));
|
||||
#endif
|
||||
ext_lfs_mounted = false;
|
||||
|
||||
const struct flash_area *fap;
|
||||
int rc;
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(lfs_partition)
|
||||
rc = flash_area_open(PARTITION_ID(lfs_partition), &fap);
|
||||
if (rc == 0) {
|
||||
LOG_INF("Formatting LFS partition (%u bytes)", (unsigned)fap->fa_size);
|
||||
flash_area_flatten(fap, 0, fap->fa_size);
|
||||
flash_area_close(fap);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(storage_partition)
|
||||
rc = flash_area_open(PARTITION_ID(storage_partition), &fap);
|
||||
if (rc == 0) {
|
||||
LOG_INF("Formatting NVS storage (%u bytes)", (unsigned)fap->fa_size);
|
||||
flash_area_flatten(fap, 0, fap->fa_size);
|
||||
flash_area_close(fap);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(qspi_storage_partition)
|
||||
/* QSPI if present (any platform) */
|
||||
rc = flash_area_open(PARTITION_ID(qspi_storage_partition), &fap);
|
||||
if (rc == 0) {
|
||||
LOG_INF("Formatting QSPI (%u bytes, may take a while)", (unsigned)fap->fa_size);
|
||||
flash_area_flatten(fap, 0, fap->fa_size);
|
||||
flash_area_close(fap);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Remount: littlefs_mount() auto-formats on blank flash, then mounts. */
|
||||
rc = fs_mount(&FS_FSTAB_ENTRY(DT_NODELABEL(lfs)));
|
||||
bool mounted = (rc == 0);
|
||||
if (mounted) {
|
||||
lfs_mounted = true;
|
||||
}
|
||||
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs))
|
||||
/* Remount external QSPI too. We unmounted it above and flattened its
|
||||
* partition, so it must be re-mounted here — otherwise a runtime format
|
||||
* (factory reset, or the first-boot "no prefs" auto-format) leaves /ext
|
||||
* unmounted for the rest of the session. begin() then reads
|
||||
* ext_lfs_mounted=false and the store falls back to internal /lfs, so
|
||||
* contacts/channels save to /lfs and get needlessly migrated back to /ext
|
||||
* on the next boot ("Migrating contacts to external storage" churn). */
|
||||
{
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(qspi_lfs));
|
||||
int ext_rc = fs_mount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs)));
|
||||
if (is_mounted(extMountPoint())) {
|
||||
ext_lfs_mounted = true;
|
||||
LOG_INF("formatFileSystem: /ext remounted (rc=%d)", ext_rc);
|
||||
} else {
|
||||
ext_lfs_mounted = false;
|
||||
LOG_ERR("formatFileSystem: /ext remount failed (rc=%d)", ext_rc);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
lfs_mounted = mounted;
|
||||
ext_lfs_mounted = ext_mounted;
|
||||
|
||||
LOG_INF("formatFileSystem: mount() returned %d", mounted ? 1 : 0);
|
||||
return mounted;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephyrFsFormat - shared factory-format of every ZephCore storage region.
|
||||
*/
|
||||
|
||||
#include "ZephyrFsFormat.h"
|
||||
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
LOG_MODULE_REGISTER(zephcore_fs_format, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL);
|
||||
|
||||
#define LFS_MNT_POINT "/lfs"
|
||||
#define EXT_MNT_POINT "/ext"
|
||||
|
||||
/* fs_mount() can return 0 having mounted nothing useful, and a remount that
|
||||
* silently failed would leave the caller reporting a healthy store over an
|
||||
* unmounted volume. Ask the VFS instead of trusting the return code. */
|
||||
static bool is_mounted(const char *mount_point)
|
||||
{
|
||||
struct fs_statvfs stat;
|
||||
|
||||
return fs_statvfs(mount_point, &stat) == 0;
|
||||
}
|
||||
|
||||
static void flatten(uint8_t id, const char *tag)
|
||||
{
|
||||
const struct flash_area *fap;
|
||||
int rc = flash_area_open(id, &fap);
|
||||
|
||||
if (rc != 0) {
|
||||
LOG_WRN("format: flash_area_open(%s) failed: %d", tag, rc);
|
||||
return;
|
||||
}
|
||||
LOG_INF("format: erasing %s (%u bytes)", tag, (unsigned)fap->fa_size);
|
||||
rc = flash_area_flatten(fap, 0, fap->fa_size);
|
||||
if (rc != 0) {
|
||||
LOG_ERR("format: flatten(%s) failed: %d", tag, rc);
|
||||
}
|
||||
flash_area_close(fap);
|
||||
}
|
||||
|
||||
bool zephcore_fs_format_all(bool *out_ext_mounted)
|
||||
{
|
||||
LOG_INF("zephcore_fs_format_all: starting...");
|
||||
|
||||
if (out_ext_mounted) {
|
||||
*out_ext_mounted = false;
|
||||
}
|
||||
|
||||
/* Properly unmount from Zephyr's VFS before erasing flash. Clearing a
|
||||
* local "mounted" flag is not enough — Zephyr would still hold /lfs
|
||||
* mounted, so flash_area_flatten destroys the on-flash superblock while
|
||||
* LittleFS considers itself active. Every subsequent file op then hits
|
||||
* the erased blocks and logs "Corrupted dir pair at {0x0, 0x1}".
|
||||
* FS_FSTAB_DECLARE_ENTRY exposes the non-static mount struct generated
|
||||
* from the DTS fstab; fs_mount() on a blank partition auto-formats
|
||||
* (littlefs_fs.c: lfs_mount fail -> lfs_format -> lfs_mount). */
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(lfs));
|
||||
fs_unmount(&FS_FSTAB_ENTRY(DT_NODELABEL(lfs)));
|
||||
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs))
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(qspi_lfs));
|
||||
fs_unmount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs)));
|
||||
#endif
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(lfs_partition)
|
||||
flatten(PARTITION_ID(lfs_partition), "lfs_partition");
|
||||
#endif
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(storage_partition)
|
||||
/* BLE bonds (NVS). A factory reset should clear them too; the caller
|
||||
* reboots so NVS and the BT stack re-init clean. */
|
||||
flatten(PARTITION_ID(storage_partition), "storage_partition");
|
||||
#endif
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(qspi_storage_partition)
|
||||
flatten(PARTITION_ID(qspi_storage_partition), "qspi_storage_partition");
|
||||
#endif
|
||||
|
||||
/* Remount: littlefs_mount() auto-formats blank flash, then mounts. */
|
||||
int rc = fs_mount(&FS_FSTAB_ENTRY(DT_NODELABEL(lfs)));
|
||||
bool mounted = is_mounted(LFS_MNT_POINT);
|
||||
|
||||
if (mounted) {
|
||||
LOG_INF("format: %s remounted (rc=%d)", LFS_MNT_POINT, rc);
|
||||
} else {
|
||||
LOG_ERR("format: %s remount FAILED (rc=%d)", LFS_MNT_POINT, rc);
|
||||
}
|
||||
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs))
|
||||
/* Remount external QSPI too. We unmounted it above and flattened its
|
||||
* partition, so it must be re-mounted here — otherwise a runtime format
|
||||
* (factory reset, or the first-boot "no prefs" auto-format) leaves /ext
|
||||
* unmounted for the rest of the session. begin() then reads
|
||||
* ext_lfs_mounted=false and the store falls back to internal /lfs, so
|
||||
* contacts/channels save to /lfs and get needlessly migrated back to
|
||||
* /ext on the next boot ("Migrating contacts to external storage"). */
|
||||
{
|
||||
int ext_rc = fs_mount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs)));
|
||||
bool ext_mounted = is_mounted(EXT_MNT_POINT);
|
||||
|
||||
if (ext_mounted) {
|
||||
LOG_INF("format: %s remounted (rc=%d)", EXT_MNT_POINT, ext_rc);
|
||||
} else {
|
||||
LOG_ERR("format: %s remount failed (rc=%d)", EXT_MNT_POINT, ext_rc);
|
||||
}
|
||||
if (out_ext_mounted) {
|
||||
*out_ext_mounted = ext_mounted;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return mounted;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephyrFsFormat - shared factory-format of every ZephCore storage region.
|
||||
*
|
||||
* Lives outside ZephyrDataStore because the repeater, room server and observer
|
||||
* build RepeaterDataStore instead (see CMakeLists role blocks) and must not
|
||||
* each grow their own half-implementation of this.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Erase every ZephCore storage region and remount the filesystems.
|
||||
*
|
||||
* Unmounts /lfs (and /ext when the board has QSPI), flattens lfs_partition,
|
||||
* storage_partition (BLE bonds NVS) and qspi_storage_partition where each
|
||||
* exists, then remounts. A blank LittleFS partition is auto-formatted by
|
||||
* fs_mount(), so the volume comes back empty rather than corrupt.
|
||||
*
|
||||
* This is the only path that erases the LittleFS *volume*. Deleting files
|
||||
* cannot recover a volume another firmware has written into — on nRF52840 the
|
||||
* Adafruit core's InternalFileSystem lives at 0xED000, inside our 0xD4000
|
||||
* lfs_partition, and its format() scribbles our top 7 blocks.
|
||||
*
|
||||
* @param out_ext_mounted optional; receives whether /ext came back mounted.
|
||||
* Set to false on boards with no QSPI.
|
||||
* @return true if /lfs is mounted afterwards.
|
||||
*/
|
||||
bool zephcore_fs_format_all(bool *out_ext_mounted);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
#include "RepeaterDataStore.h"
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
@@ -37,6 +38,37 @@ bool RepeaterDataStore::begin() {
|
||||
|
||||
const char* RepeaterDataStore::getBasePath() const { return BASE_PATH; }
|
||||
|
||||
static bool fileExists(const char* path) {
|
||||
struct fs_dirent entry;
|
||||
return fs_stat(path, &entry) == 0;
|
||||
}
|
||||
|
||||
bool RepeaterDataStore::hasRoleData() const {
|
||||
char path[64];
|
||||
|
||||
/* Only THIS role's files count. A companion volume does not: the roles
|
||||
* are deliberately not interchangeable, and a companion's contacts and
|
||||
* blob cache would eat into the same 128 KB the repeater needs, so a
|
||||
* repeater booting onto a companion volume formats it. The reverse
|
||||
* already happens — ZephyrDataStore::hasPrefs() tests /lfs/new_prefs,
|
||||
* which a repeater volume never has.
|
||||
*
|
||||
* Repeater, room server and observer DO share this store and base path;
|
||||
* they use the same prefs layout, so switching among them keeps the
|
||||
* node's identity, which is what an operator wants.
|
||||
*
|
||||
* Self-limiting: loadPrefs() persists defaults on boot 1 and main_*.cpp
|
||||
* saves a generated identity on the same boot, so after one successful
|
||||
* boot at least one of these exists and the check never fires again. */
|
||||
static const char* const ours[] = { "prefs", "_main.id" };
|
||||
for (size_t i = 0; i < ARRAY_SIZE(ours); i++) {
|
||||
snprintf(path, sizeof(path), "%s/%s", BASE_PATH, ours[i]);
|
||||
if (fileExists(path)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* RepeaterDataStore::getAclPath() const {
|
||||
static char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s/acl", BASE_PATH);
|
||||
@@ -412,37 +444,28 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
|
||||
}
|
||||
|
||||
bool RepeaterDataStore::formatFileSystem() {
|
||||
LOG_WRN("Factory reset: erasing repeater data at %s", BASE_PATH);
|
||||
LOG_WRN("Factory reset: erasing all storage");
|
||||
|
||||
struct fs_dir_t dir;
|
||||
fs_dir_t_init(&dir);
|
||||
|
||||
int ret = fs_opendir(&dir, BASE_PATH);
|
||||
if (ret < 0) {
|
||||
LOG_WRN("No repeater directory to erase");
|
||||
return true;
|
||||
/* Erase the LittleFS *volume*, not just our files. The old loop walked
|
||||
* /lfs/repeater/ with fs_unlink, which left the volume itself untouched:
|
||||
* it could not recover a volume another firmware had written into (on
|
||||
* nRF52840 the Adafruit core's filesystem overlaps the top of ours), and
|
||||
* it left /lfs/settings, stale companion files and all of /ext behind.
|
||||
* Shared with the companion so all four roles erase the same regions. */
|
||||
bool mounted = zephcore_fs_format_all(nullptr);
|
||||
if (!mounted) {
|
||||
LOG_ERR("Factory reset: /lfs did not remount");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct fs_dirent entry;
|
||||
char path[280];
|
||||
|
||||
while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') {
|
||||
snprintf(path, sizeof(path), "%s/%s", BASE_PATH, entry.name);
|
||||
LOG_INF("Deleting %s", path);
|
||||
fs_unlink(path);
|
||||
/* The format took /lfs/repeater with it. Re-create it now rather than
|
||||
* relying on the reboot: the CLI defers the reset so the reply can be
|
||||
* transmitted, and anything that saves in that window needs the dir. */
|
||||
_initialized = false;
|
||||
if (!begin()) {
|
||||
LOG_ERR("Factory reset: could not re-create %s", BASE_PATH);
|
||||
return false;
|
||||
}
|
||||
fs_closedir(&dir);
|
||||
|
||||
#if FIXED_PARTITION_EXISTS(storage_partition)
|
||||
/* Erase the NVS bonds partition too — a factory reset should clear BLE
|
||||
* bonds, not just repeater files. Caller reboots so NVS re-inits clean. */
|
||||
const struct flash_area *fap;
|
||||
if (flash_area_open(PARTITION_ID(storage_partition), &fap) == 0) {
|
||||
LOG_INF("Formatting NVS storage (%u bytes)", (unsigned)fap->fa_size);
|
||||
flash_area_flatten(fap, 0, fap->fa_size);
|
||||
flash_area_close(fap);
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INF("Repeater data erased");
|
||||
return true;
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
* RepeaterDataStore - Filesystem storage for repeater
|
||||
*
|
||||
* Uses /lfs/repeater/ prefix to keep data separate from companion.
|
||||
* This allows flashing back and forth between roles without corruption.
|
||||
* Uses /lfs/repeater/ prefix to keep data separate from companion, so the
|
||||
* two prefs layouts (301 B here, 163 B there, different field order) can
|
||||
* never be read through each other.
|
||||
*
|
||||
* The roles are NOT interchangeable: booting a repeater onto a companion
|
||||
* volume formats it, and vice versa. They are different kinds of node, and
|
||||
* they share one 128 KB LittleFS volume - a companion's contacts and blob
|
||||
* cache would crowd out repeater writes (-ENOSPC), not corrupt them. Save
|
||||
* your identity before switching roles.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
@@ -39,6 +46,13 @@ public:
|
||||
/* Factory reset - erase all repeater data */
|
||||
bool formatFileSystem();
|
||||
|
||||
/* True if this LittleFS volume already holds THIS role's data. False
|
||||
* means the volume belongs to something else - a fresh chip, a companion,
|
||||
* or a node that was running Arduino MeshCore, whose nRF52 filesystems
|
||||
* overlap our lfs_partition (devdocs/HANDOVER_lfs_arduino_overlap.md).
|
||||
* Callers format on false; see the note on BASE_PATH below. */
|
||||
bool hasRoleData() const;
|
||||
|
||||
/* Get base path for repeater storage */
|
||||
const char* getBasePath() const;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
LOG_MODULE_REGISTER(zephcore_observer_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
|
||||
#include <app/RepeaterDataStore.h>
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
#include <app/ObserverMesh.h>
|
||||
#include <adapters/clock/ZephyrRTCClock.h>
|
||||
#include <mesh/RadioIncludes.h>
|
||||
@@ -306,6 +307,24 @@ int main(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* First boot on a volume that is not this role's - a fresh chip, a
|
||||
* companion, or a node that was running Arduino MeshCore, whose nRF52
|
||||
* filesystems overlap our lfs_partition
|
||||
* (devdocs/HANDOVER_lfs_arduino_overlap.md). Erase everything so we
|
||||
* start from a known state: Zephyr's automount only
|
||||
* auto-formats the LittleFS volume when it fails to mount, and never
|
||||
* touches storage_partition (BLE bonds NVS) or QSPI.
|
||||
*
|
||||
* Self-limiting, so it needs no "done" marker: the identity is generated
|
||||
* and saved a few lines below, and loadPrefs() persists defaults on the
|
||||
* same boot, so the next boot sees this role's data and skips this. */
|
||||
if (!data_store.hasRoleData()) {
|
||||
LOG_WRN("Volume holds no data for this role - formatting before first boot");
|
||||
if (!zephcore_fs_format_all(nullptr)) {
|
||||
LOG_ERR("First-boot format failed - /lfs is not mounted");
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize LittleFS data store */
|
||||
if (!data_store.begin()) {
|
||||
LOG_ERR("RepeaterDataStore init failed");
|
||||
|
||||
@@ -708,7 +708,17 @@ public:
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
const char* getRole() override { return "companion"; }
|
||||
bool formatFileSystem() override { return data_store.formatFileSystem(); }
|
||||
/* CLI `erase`. Re-stamp the init marker on success, exactly as
|
||||
* factoryReset() does for the BLE opcode path: the format takes
|
||||
* /lfs/_zc_init with it, so without this the post-reboot first-boot check
|
||||
* sees "no marker, no prefs" and runs a second, pointless full format. */
|
||||
bool formatFileSystem() override {
|
||||
bool ok = data_store.formatFileSystem();
|
||||
if (ok) {
|
||||
data_store.writeInitMarker();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Advert — the companion can originate its own self-advert. delay_millis is
|
||||
* unused (companion sends flood at 0 ms / zero-hop immediately, matching the
|
||||
|
||||
@@ -50,6 +50,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
|
||||
#endif
|
||||
|
||||
#include <app/RepeaterDataStore.h>
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
#include <app/RepeaterMesh.h>
|
||||
#include <adapters/clock/ZephyrRTCClock.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
@@ -643,6 +644,24 @@ int main(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* First boot on a volume that is not this role's - a fresh chip, a
|
||||
* companion, or a node that was running Arduino MeshCore, whose nRF52
|
||||
* filesystems overlap our lfs_partition
|
||||
* (devdocs/HANDOVER_lfs_arduino_overlap.md). Erase everything so we
|
||||
* start from a known state: Zephyr's automount only
|
||||
* auto-formats the LittleFS volume when it fails to mount, and never
|
||||
* touches storage_partition (BLE bonds NVS) or QSPI.
|
||||
*
|
||||
* Self-limiting, so it needs no "done" marker: the identity is generated
|
||||
* and saved a few lines below, and loadPrefs() persists defaults on the
|
||||
* same boot, so the next boot sees this role's data and skips this. */
|
||||
if (!data_store.hasRoleData()) {
|
||||
LOG_WRN("Volume holds no data for this role - formatting before first boot");
|
||||
if (!zephcore_fs_format_all(nullptr)) {
|
||||
LOG_ERR("First-boot format failed - /lfs is not mounted");
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize repeater data store */
|
||||
if (!data_store.begin()) {
|
||||
LOG_ERR("RepeaterDataStore init failed");
|
||||
|
||||
@@ -50,6 +50,7 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
|
||||
#endif
|
||||
|
||||
#include <app/RepeaterDataStore.h>
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
#include <app/RoomServerMesh.h>
|
||||
#include <adapters/clock/ZephyrRTCClock.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
@@ -529,6 +530,24 @@ int main(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* First boot on a volume that is not this role's - a fresh chip, a
|
||||
* companion, or a node that was running Arduino MeshCore, whose nRF52
|
||||
* filesystems overlap our lfs_partition
|
||||
* (devdocs/HANDOVER_lfs_arduino_overlap.md). Erase everything so we
|
||||
* start from a known state: Zephyr's automount only
|
||||
* auto-formats the LittleFS volume when it fails to mount, and never
|
||||
* touches storage_partition (BLE bonds NVS) or QSPI.
|
||||
*
|
||||
* Self-limiting, so it needs no "done" marker: the identity is generated
|
||||
* and saved a few lines below, and loadPrefs() persists defaults on the
|
||||
* same boot, so the next boot sees this role's data and skips this. */
|
||||
if (!data_store.hasRoleData()) {
|
||||
LOG_WRN("Volume holds no data for this role - formatting before first boot");
|
||||
if (!zephcore_fs_format_all(nullptr)) {
|
||||
LOG_ERR("First-boot format failed - /lfs is not mounted");
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize repeater data store */
|
||||
if (!data_store.begin()) {
|
||||
LOG_ERR("RepeaterDataStore init failed");
|
||||
|
||||
Reference in New Issue
Block a user