Harden nRF52 filesystem recovery and flash writes
This commit is contained in:
1 parent
4c645b0d14
commit
80f37d4374
24 files changed
+1220
-218
No files matched your search
@@ -19,6 +19,10 @@
|
||||
#include <helpers/AtomicFileWriter.h>
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
#include <helpers/nrf52/InternalSecondaryFsRepair.h>
|
||||
#include "ResilientInternalExtraFS.h"
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -154,18 +158,98 @@ void DataStore::begin() {
|
||||
|
||||
bool primary_ready = validateLfsFilesystem(_fs);
|
||||
if (!primary_ready) {
|
||||
// A traversal error can be transient, and formatting here would erase the
|
||||
// only identity before MyMesh can distinguish recovery from a fresh boot.
|
||||
// Preserve the filesystem and fail closed; a reboot may recover it, while
|
||||
// an explicit factory reset remains the destructive recovery operation.
|
||||
for (uint8_t retry = 0; retry < 3 && !primary_ready; ++retry) {
|
||||
delay(3000);
|
||||
primary_ready = validateLfsFilesystem(_fs);
|
||||
}
|
||||
if (!primary_ready) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"DataStore: primary metadata remains unreadable after delayed retries; reinitializing storage");
|
||||
primary_ready = _fs->format() && validateLfsFilesystem(_fs);
|
||||
if (!primary_ready) {
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary =
|
||||
mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary == nullptr) {
|
||||
_primary_storage_unavailable = true;
|
||||
_identity_creation_blocked = true;
|
||||
_contact_load_incomplete = true;
|
||||
_prefs_load_incomplete = true;
|
||||
_fsExtra = nullptr;
|
||||
return;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"DataStore: verified primary erase/write failed; using volatile RAM filesystem");
|
||||
useVolatilePrimaryFS(ram_primary->filesystem());
|
||||
primary_ready = true;
|
||||
} else {
|
||||
_identity_creation_blocked = false;
|
||||
_contact_load_incomplete = false;
|
||||
_prefs_load_incomplete = false;
|
||||
#if defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
if (_configuredFsExtra != nullptr) {
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
const bool scan = !extra->pageMapReady();
|
||||
if (scan) extra->resetPageMapForDestructiveRecovery();
|
||||
else extra->requirePageMapRewrite();
|
||||
if (!reinitializeInternalExtraFS(scan)) disableSecondaryFS(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A mounted filesystem can still contain an unreadable identity record.
|
||||
// Keep the normal bounded reads, then try three more times three seconds
|
||||
// apart. Only after those fail may startup wipe the unusable primary store.
|
||||
mesh::LocalIdentity identity_probe;
|
||||
IdentityLoadResult identity_state =
|
||||
identity_store.loadResult("_main", identity_probe);
|
||||
if (identity_state == IdentityLoadResult::Unreadable) {
|
||||
for (uint8_t retry = 0; retry < 3; ++retry) {
|
||||
delay(3000);
|
||||
identity_state = identity_store.loadResult("_main", identity_probe);
|
||||
if (identity_state != IdentityLoadResult::Unreadable) break;
|
||||
}
|
||||
}
|
||||
if (identity_state == IdentityLoadResult::Unreadable) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"DataStore: primary LittleFS metadata is unavailable; preserving it and blocking startup writes");
|
||||
_primary_storage_unavailable = true;
|
||||
_identity_creation_blocked = true;
|
||||
_contact_load_incomplete = true;
|
||||
_prefs_load_incomplete = true;
|
||||
_fsExtra = nullptr;
|
||||
return;
|
||||
"DataStore: identity remains unreadable after delayed retries; reinitializing primary storage");
|
||||
const bool primary_reinitialized = _fs->format()
|
||||
&& validateLfsFilesystem(_fs);
|
||||
if (!primary_reinitialized) {
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary =
|
||||
mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary == nullptr) {
|
||||
_primary_storage_unavailable = true;
|
||||
_identity_creation_blocked = true;
|
||||
_contact_load_incomplete = true;
|
||||
_prefs_load_incomplete = true;
|
||||
_fsExtra = nullptr;
|
||||
return;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"DataStore: verified primary erase/write failed; using volatile RAM filesystem");
|
||||
useVolatilePrimaryFS(ram_primary->filesystem());
|
||||
} else {
|
||||
_identity_creation_blocked = false;
|
||||
_contact_load_incomplete = false;
|
||||
_prefs_load_incomplete = false;
|
||||
#if defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
if (_configuredFsExtra != nullptr) {
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
const bool scan = !extra->pageMapReady();
|
||||
if (scan) extra->resetPageMapForDestructiveRecovery();
|
||||
else extra->requirePageMapRewrite();
|
||||
if (!reinitializeInternalExtraFS(scan)) {
|
||||
// The primary identity is now a fresh authoritative install. An
|
||||
// unusable old secondary must not block that new identity.
|
||||
disableSecondaryFS(false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
// Validate primary first: automatic secondary recovery must never hide a
|
||||
@@ -257,7 +341,8 @@ static void cleanupAtomicTempFiles(FILESYSTEM* fs) {
|
||||
static const char* fixed_temp_paths[] = {
|
||||
"/_main.id.tmp", "/new_prefs.tmp", "/channels2.tmp",
|
||||
"/contacts3.tmp", "/contacts4.mig.tmp", "/adv_blobs.tmp",
|
||||
"/.extrafs.mig.tmp"};
|
||||
"/.extrafs.mig.tmp", "/extrafs.badpages.tmp",
|
||||
"/extrafs.badpages.bak.tmp"};
|
||||
for (size_t i = 0; i < sizeof(fixed_temp_paths) / sizeof(fixed_temp_paths[0]); i++) {
|
||||
if (fs->exists(fixed_temp_paths[i])) fs->remove(fixed_temp_paths[i]);
|
||||
}
|
||||
@@ -283,7 +368,6 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
if (_primary_storage_unavailable
|
||||
|| !extra->pageMapReady()
|
||||
|| !mesh::storage::isExpectedInternalExtraFsGeometry(
|
||||
extra->getFlashAddr(), extra->getFlashSize(), extra->getBlockSize())
|
||||
|| !mesh::storage::isInternalExtraFsReservedByApplication(
|
||||
@@ -293,6 +377,21 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!extra->pageMapReady()) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"DataStore: both ExtraFS page maps are unreadable; scanning and rebuilding reserved secondary storage");
|
||||
extra->resetPageMapForDestructiveRecovery();
|
||||
if (!reinitializeInternalExtraFS(true)) {
|
||||
disableSecondaryFS(true);
|
||||
return false;
|
||||
}
|
||||
_secondary_authority_unknown = false;
|
||||
_contact_load_incomplete = false;
|
||||
_identity_creation_blocked = false;
|
||||
_prefs_load_incomplete = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (extra->recoveryPending()) {
|
||||
MESH_DEBUG_PRINTLN("DataStore: running requested boot-time ExtraFS scan");
|
||||
if (!reinitializeInternalExtraFS(true)) {
|
||||
@@ -330,6 +429,10 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
}
|
||||
|
||||
extra->acknowledgeRecoveredBootHint();
|
||||
if (extra->pageMapNeedsSave() && !extra->savePageMap(*_fs)) {
|
||||
disableSecondaryFS(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// This runs only before migration and user-data loading. Clear the initial
|
||||
// mount quarantine, not errors from a later incomplete contact/prefs load.
|
||||
@@ -349,13 +452,19 @@ bool DataStore::reinitializeInternalExtraFS(bool scan_physical_pages) {
|
||||
if (!mesh::storage::isExpectedInternalExtraFsGeometry(
|
||||
extra->getFlashAddr(), extra->getFlashSize(),
|
||||
extra->getBlockSize())
|
||||
|| !extra->pageMapReady()
|
||||
|| !mesh::storage::isInternalExtraFsReservedByApplication(
|
||||
(uint32_t)(uintptr_t)__flash_arduino_end)) {
|
||||
_fsExtra = nullptr;
|
||||
MESH_DEBUG_PRINTLN("DataStore: refusing internal ExtraFS repair with unexpected geometry");
|
||||
return false;
|
||||
}
|
||||
if (!extra->pageMapReady()) {
|
||||
if (!scan_physical_pages) {
|
||||
_fsExtra = nullptr;
|
||||
return false;
|
||||
}
|
||||
extra->resetPageMapForDestructiveRecovery();
|
||||
}
|
||||
|
||||
// A physical test is destructive and must run only during early boot, before
|
||||
// radio/UI tasks can issue competing SoftDevice flash operations. Ordinary
|
||||
@@ -422,6 +531,21 @@ void DataStore::markPrimaryFSUnavailable() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
void DataStore::useVolatilePrimaryFS(FILESYSTEM& fs) {
|
||||
_fs = &fs;
|
||||
identity_store.useFileSystem(fs);
|
||||
_fsExtra = nullptr;
|
||||
_configuredFsExtra = nullptr;
|
||||
_volatile_primary_fs = true;
|
||||
_primary_storage_unavailable = false;
|
||||
_secondary_authority_unknown = false;
|
||||
_identity_creation_blocked = false;
|
||||
_contact_load_incomplete = false;
|
||||
_prefs_load_incomplete = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void DataStore::disableSecondaryFS(bool authority_unknown) {
|
||||
_fsExtra = nullptr;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
@@ -594,10 +718,22 @@ bool DataStore::formatFileSystem() {
|
||||
// Factory reset/rebuild is already an explicit destructive operation. Use
|
||||
// the configured pointer so it also clears and reactivates an ExtraFS which
|
||||
// normal boot deliberately quarantined after failed traversal validation.
|
||||
const bool secondary_success = _configuredFsExtra == nullptr
|
||||
|| reinitializeInternalExtraFS();
|
||||
bool secondary_success = true;
|
||||
if (primary_success && _configuredFsExtra != nullptr) {
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
if (!extra->pageMapReady()) {
|
||||
extra->resetPageMapForDestructiveRecovery();
|
||||
} else {
|
||||
// The primary format erased both map files. Recreate them before the
|
||||
// secondary format so a reboot never guesses a retired page is healthy.
|
||||
extra->requirePageMapRewrite();
|
||||
}
|
||||
secondary_success = reinitializeInternalExtraFS();
|
||||
}
|
||||
#else
|
||||
const bool secondary_success = _fsExtra == nullptr || _fsExtra->format();
|
||||
const bool secondary_success = !primary_success
|
||||
|| _fsExtra == nullptr || _fsExtra->format();
|
||||
#endif
|
||||
const bool success = primary_success && secondary_success;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
@@ -688,7 +824,7 @@ bool DataStore::requestInternalExtraFSBootScan() {
|
||||
}
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
return extra->requestBootScan();
|
||||
return extra->requestBootScan(true);
|
||||
}
|
||||
|
||||
bool DataStore::formatInternalExtraFSHealth(char* reply,
|
||||
|
||||
@@ -76,6 +76,7 @@ class DataStore
|
||||
bool _contact_load_incomplete = false;
|
||||
bool _primary_storage_unavailable = false;
|
||||
bool _secondary_authority_unknown = false;
|
||||
bool _volatile_primary_fs = false;
|
||||
uint32_t _contact_page_generations[mesh::storage::CONTACT_PAGE_COUNT];
|
||||
bool _legacy_contacts_pending_cleanup;
|
||||
bool _legacy_migration_ready;
|
||||
@@ -112,6 +113,10 @@ public:
|
||||
#endif
|
||||
FILESYSTEM* getPrimaryFS() const { return _fs; }
|
||||
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
|
||||
#if defined(NRF52_PLATFORM)
|
||||
void useVolatilePrimaryFS(FILESYSTEM& fs);
|
||||
bool isVolatilePrimaryFS() const { return _volatile_primary_fs; }
|
||||
#endif
|
||||
void markPrimaryFSUnavailable();
|
||||
void disableSecondaryFS(bool authority_unknown = true);
|
||||
bool loadMainIdentity(mesh::LocalIdentity &identity);
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
#include <helpers/StatsFormatHelper.h>
|
||||
#include <helpers/UsbAsciiBinarySwitch.h>
|
||||
#include <helpers/UsbLogging.h>
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#endif
|
||||
#if defined(ENABLE_OTA)
|
||||
#include <helpers/ota/OtaContext.h>
|
||||
#endif
|
||||
@@ -1853,6 +1856,17 @@ void MyMesh::begin(bool has_display, bool radio_available) {
|
||||
const bool prefs_ready =
|
||||
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// A volatile primary filesystem is the final recovery mode after the
|
||||
// physical medium has exhausted its delayed reads and destructive repair.
|
||||
// Apply the diagnostic name before BLE, USB, MQTT, or adverts consume it.
|
||||
if (_store->isVolatilePrimaryFS()) {
|
||||
strncpy(_prefs.node_name, mesh::storage::BAD_FILESYSTEM_NODE_NAME,
|
||||
sizeof(_prefs.node_name));
|
||||
_prefs.node_name[sizeof(_prefs.node_name) - 1] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// v1.17.1.2 repairs the Companion default-off regression for both fresh
|
||||
// installs and devices that already persisted the regressed value. The
|
||||
// appended policy marker makes this a one-time migration, so a later
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/AtomicFileWriter.h>
|
||||
#include <helpers/nrf52/InternalFlashStatus.h>
|
||||
#include <helpers/nrf52/SoftDeviceState.h>
|
||||
#include <nrf_soc.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
constexpr char PAGE_MAP_PATH[] = "/extrafs.badpages";
|
||||
constexpr uint32_t PAGE_MAP_MAGIC = 0x31475042UL; // "BPG1" on nRF52
|
||||
constexpr char PAGE_MAP_BACKUP_PATH[] = "/extrafs.badpages.bak";
|
||||
constexpr uint32_t PAGE_MAP_V1_MAGIC = 0x31475042UL; // "BPG1" on nRF52
|
||||
constexpr uint32_t PAGE_MAP_V2_MAGIC = 0x32475042UL; // "BPG2" on nRF52
|
||||
constexpr uint8_t PAGE_MAP_READ_ATTEMPTS = 3;
|
||||
#if defined(NRF52840_XXAA)
|
||||
constexpr uint32_t PRIMARY_START = 0xED000UL;
|
||||
#else
|
||||
@@ -67,17 +71,103 @@ int primaryErase(const struct lfs_config*, lfs_block_t block) {
|
||||
|
||||
int primarySync(const struct lfs_config*) {
|
||||
FlashLock lock;
|
||||
flash_nrf5x_flush();
|
||||
return 0;
|
||||
return mesh_flash_nrf5x_flush_checked() ? 0 : LFS_ERR_IO;
|
||||
}
|
||||
|
||||
struct PageMapRecord {
|
||||
struct LegacyPageMapRecord {
|
||||
uint32_t magic;
|
||||
uint32_t bad_pages;
|
||||
uint32_t pending_pages;
|
||||
uint32_t check;
|
||||
};
|
||||
|
||||
struct PageMapRecord {
|
||||
uint32_t magic;
|
||||
uint32_t generation;
|
||||
uint32_t bad_pages;
|
||||
uint32_t pending_pages;
|
||||
uint32_t check;
|
||||
};
|
||||
|
||||
enum class PageMapReadState : uint8_t { Missing, Valid, Invalid };
|
||||
|
||||
struct PageMapCandidate {
|
||||
PageMapReadState state = PageMapReadState::Invalid;
|
||||
uint32_t generation = 0;
|
||||
uint32_t bad_pages = 0;
|
||||
uint32_t pending_pages = 0;
|
||||
bool legacy = false;
|
||||
};
|
||||
|
||||
bool validPageMapValues(uint32_t bad_pages, uint32_t pending_pages) {
|
||||
return (bad_pages & ~mesh::storage::INTERNAL_EXTRAFS_PAGE_MASK) == 0
|
||||
&& (pending_pages
|
||||
& ~(mesh::storage::INTERNAL_EXTRAFS_PAGE_MASK | (1UL << 31))) == 0
|
||||
&& (bad_pages & pending_pages
|
||||
& mesh::storage::INTERNAL_EXTRAFS_PAGE_MASK) == 0
|
||||
&& mesh::storage::countInternalExtraFsBadPages(bad_pages) <= 4;
|
||||
}
|
||||
|
||||
PageMapCandidate readPageMapCandidate(Adafruit_LittleFS& primary,
|
||||
const char* path) {
|
||||
PageMapCandidate candidate;
|
||||
for (uint8_t attempt = 0; attempt < PAGE_MAP_READ_ATTEMPTS; ++attempt) {
|
||||
struct lfs_info info = {};
|
||||
primary._lockFS();
|
||||
const int stat_result = lfs_stat(primary._getFS(), path, &info);
|
||||
primary._unlockFS();
|
||||
if (stat_result == LFS_ERR_NOENT) {
|
||||
candidate.state = PageMapReadState::Missing;
|
||||
return candidate;
|
||||
}
|
||||
if (stat_result != LFS_ERR_OK || info.type != LFS_TYPE_REG
|
||||
|| (info.size != sizeof(PageMapRecord)
|
||||
&& info.size != sizeof(LegacyPageMapRecord))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File file = primary.open(path, FILE_O_READ);
|
||||
if (!file) continue;
|
||||
if (info.size == sizeof(PageMapRecord)) {
|
||||
PageMapRecord record = {};
|
||||
const int count = file.read(reinterpret_cast<uint8_t*>(&record),
|
||||
sizeof(record));
|
||||
file.close();
|
||||
if (count == sizeof(record) && record.magic == PAGE_MAP_V2_MAGIC
|
||||
&& record.check
|
||||
== ~(record.magic ^ record.generation ^ record.bad_pages
|
||||
^ record.pending_pages)
|
||||
&& validPageMapValues(record.bad_pages, record.pending_pages)) {
|
||||
candidate.state = PageMapReadState::Valid;
|
||||
candidate.generation = record.generation;
|
||||
candidate.bad_pages = record.bad_pages;
|
||||
candidate.pending_pages = record.pending_pages;
|
||||
return candidate;
|
||||
}
|
||||
} else {
|
||||
LegacyPageMapRecord record = {};
|
||||
const int count = file.read(reinterpret_cast<uint8_t*>(&record),
|
||||
sizeof(record));
|
||||
file.close();
|
||||
if (count == sizeof(record) && record.magic == PAGE_MAP_V1_MAGIC
|
||||
&& record.check
|
||||
== ~(record.magic ^ record.bad_pages ^ record.pending_pages)
|
||||
&& validPageMapValues(record.bad_pages, record.pending_pages)) {
|
||||
candidate.state = PageMapReadState::Valid;
|
||||
candidate.bad_pages = record.bad_pages;
|
||||
candidate.pending_pages = record.pending_pages;
|
||||
candidate.legacy = true;
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
bool newerGeneration(uint32_t candidate, uint32_t reference) {
|
||||
return static_cast<int32_t>(candidate - reference) > 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
using mesh::storage::countInternalExtraFsBadPages;
|
||||
@@ -103,7 +193,7 @@ ResilientInternalExtraFS::ResilientInternalExtraFS(
|
||||
// bootloader leaves this register alone for an ordinary app reset.
|
||||
const uint8_t retained = static_cast<uint8_t>(NRF_POWER->GPREGRET2);
|
||||
_boot_marker_at_init = retained;
|
||||
if (retained == BOOT_SCAN_ALL) {
|
||||
if (retained == BOOT_SCAN_ALL || retained == BOOT_SCAN_REPEAT) {
|
||||
_boot_scan_requested = true;
|
||||
_boot_scan_forced = true;
|
||||
} else if (retained >= BOOT_SCAN_BAD_PAGE_BASE
|
||||
@@ -132,37 +222,43 @@ bool ResilientInternalExtraFS::loadPageMap(Adafruit_LittleFS& primary) {
|
||||
_map_ready = false;
|
||||
_bad_pages = 0;
|
||||
_pending_pages = 0;
|
||||
struct lfs_info info;
|
||||
const int stat_result = lfs_stat(primary._getFS(), PAGE_MAP_PATH, &info);
|
||||
if (stat_result == LFS_ERR_NOENT) {
|
||||
_map_generation = 0;
|
||||
_map_repair_needed = false;
|
||||
|
||||
const PageMapCandidate main = readPageMapCandidate(primary, PAGE_MAP_PATH);
|
||||
const PageMapCandidate backup =
|
||||
readPageMapCandidate(primary, PAGE_MAP_BACKUP_PATH);
|
||||
if (main.state == PageMapReadState::Missing
|
||||
&& backup.state == PageMapReadState::Missing) {
|
||||
_configure_lfs();
|
||||
_map_ready = true;
|
||||
return true;
|
||||
}
|
||||
if (stat_result != LFS_ERR_OK || info.type != LFS_TYPE_REG
|
||||
|| info.size != sizeof(PageMapRecord)) return false;
|
||||
|
||||
File file = primary.open(PAGE_MAP_PATH, FILE_O_READ);
|
||||
if (!file) return false;
|
||||
PageMapRecord record = {};
|
||||
const int read_count = file.read(reinterpret_cast<uint8_t*>(&record),
|
||||
sizeof(record));
|
||||
file.close();
|
||||
if (read_count != sizeof(record) || record.magic != PAGE_MAP_MAGIC
|
||||
|| record.check != ~(record.magic ^ record.bad_pages
|
||||
^ record.pending_pages)
|
||||
|| (record.bad_pages & ~PAGE_MASK) != 0
|
||||
|| (record.pending_pages & ~(PAGE_MASK | SCAN_REQUEST)) != 0
|
||||
|| (record.bad_pages & record.pending_pages & PAGE_MASK) != 0
|
||||
|| countInternalExtraFsBadPages(
|
||||
record.bad_pages | (record.pending_pages & PAGE_MASK))
|
||||
> MAX_BAD_PAGES) {
|
||||
const PageMapCandidate* chosen = nullptr;
|
||||
if (main.state == PageMapReadState::Valid
|
||||
&& backup.state == PageMapReadState::Valid) {
|
||||
chosen = newerGeneration(backup.generation, main.generation)
|
||||
? &backup : &main;
|
||||
_map_repair_needed = main.legacy || backup.legacy
|
||||
|| main.generation != backup.generation
|
||||
|| main.bad_pages != backup.bad_pages
|
||||
|| main.pending_pages != backup.pending_pages;
|
||||
} else if (main.state == PageMapReadState::Valid) {
|
||||
chosen = &main;
|
||||
_map_repair_needed = true;
|
||||
} else if (backup.state == PageMapReadState::Valid) {
|
||||
chosen = &backup;
|
||||
_map_repair_needed = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
_bad_pages = record.bad_pages;
|
||||
_pending_pages = record.pending_pages;
|
||||
_recorded_bad_pages = record.bad_pages;
|
||||
_recorded_pending_pages = record.pending_pages;
|
||||
|
||||
_bad_pages = chosen->bad_pages;
|
||||
_pending_pages = chosen->pending_pages;
|
||||
_map_generation = chosen->generation;
|
||||
_recorded_bad_pages = _bad_pages;
|
||||
_recorded_pending_pages = _pending_pages;
|
||||
_configure_lfs();
|
||||
_map_ready = true;
|
||||
return true;
|
||||
@@ -170,27 +266,47 @@ bool ResilientInternalExtraFS::loadPageMap(Adafruit_LittleFS& primary) {
|
||||
|
||||
bool ResilientInternalExtraFS::savePageMap(Adafruit_LittleFS& primary) {
|
||||
if (!_map_ready) return false;
|
||||
const uint32_t generation = _map_generation + 1;
|
||||
const PageMapRecord record = {
|
||||
PAGE_MAP_MAGIC, _bad_pages, _pending_pages,
|
||||
~(PAGE_MAP_MAGIC ^ _bad_pages ^ _pending_pages)};
|
||||
mesh::AtomicFileWriter writer(&primary, PAGE_MAP_PATH);
|
||||
const bool saved = writer
|
||||
&& writer.write(reinterpret_cast<const uint8_t*>(&record),
|
||||
sizeof(record)) == sizeof(record)
|
||||
&& writer.commit();
|
||||
PAGE_MAP_V2_MAGIC, generation, _bad_pages, _pending_pages,
|
||||
~(PAGE_MAP_V2_MAGIC ^ generation ^ _bad_pages ^ _pending_pages)};
|
||||
const auto write_record = [&primary, &record](const char* path) -> bool {
|
||||
mesh::AtomicFileWriter writer(&primary, path);
|
||||
return writer
|
||||
&& writer.write(reinterpret_cast<const uint8_t*>(&record),
|
||||
sizeof(record)) == sizeof(record)
|
||||
&& writer.commit();
|
||||
};
|
||||
const bool main_saved = write_record(PAGE_MAP_PATH);
|
||||
const bool backup_saved = write_record(PAGE_MAP_BACKUP_PATH);
|
||||
const bool saved = main_saved && backup_saved;
|
||||
if (saved) {
|
||||
_map_generation = generation;
|
||||
_recorded_bad_pages = _bad_pages;
|
||||
_recorded_pending_pages = _pending_pages;
|
||||
_map_repair_needed = false;
|
||||
}
|
||||
_stage = saved ? Stage::MapSaved : Stage::MapSaveFailed;
|
||||
return saved;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::pageMapNeedsSave() const {
|
||||
return _bad_pages != _recorded_bad_pages
|
||||
return _map_repair_needed
|
||||
|| _bad_pages != _recorded_bad_pages
|
||||
|| _pending_pages != _recorded_pending_pages;
|
||||
}
|
||||
|
||||
void ResilientInternalExtraFS::resetPageMapForDestructiveRecovery() {
|
||||
_bad_pages = 0;
|
||||
_pending_pages = 0;
|
||||
_recorded_bad_pages = 0;
|
||||
_recorded_pending_pages = 0;
|
||||
_map_generation = 0;
|
||||
_map_repair_needed = true;
|
||||
_map_ready = true;
|
||||
_configure_lfs();
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::setBootScanRequest(uint8_t value) {
|
||||
uint8_t sd_enabled = 0;
|
||||
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
|
||||
@@ -218,17 +334,19 @@ bool ResilientInternalExtraFS::clearBootScanRequest() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::requestBootScan() {
|
||||
if (!_map_ready) return false;
|
||||
bool ResilientInternalExtraFS::requestBootScan(bool force_full_scan) {
|
||||
if (!_map_ready && !force_full_scan) return false;
|
||||
// Do not touch the primary filesystem while radio/BLE tasks are running.
|
||||
// The suspect 4 KiB page (if known) fits in one retained byte. A fault hint
|
||||
// first gets a non-destructive mount and traversal at boot; only an explicit
|
||||
// user scan or unrecoverable filesystem triggers the destructive test.
|
||||
uint8_t marker = BOOT_SCAN_ALL;
|
||||
if ((_pending_pages & PAGE_MASK) != 0) {
|
||||
if (!force_full_scan && (_pending_pages & PAGE_MASK) != 0) {
|
||||
for (uint8_t page = 0; page < PAGE_COUNT; ++page) {
|
||||
if ((_pending_pages & (1UL << page)) != 0) {
|
||||
marker = BOOT_SCAN_BAD_PAGE_BASE + page;
|
||||
marker = (_retained_bad_page == page)
|
||||
? BOOT_SCAN_REPEAT
|
||||
: BOOT_SCAN_BAD_PAGE_BASE + page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -241,9 +359,16 @@ void ResilientInternalExtraFS::acknowledgeRecoveredBootHint() {
|
||||
// the existing filesystem after a transient runtime flash error. The
|
||||
// hint remains a diagnostic only; it is not a bad-page verdict.
|
||||
if (!_boot_scan_forced) {
|
||||
clearBootScanRequest();
|
||||
_pending_pages &= SCAN_REQUEST;
|
||||
_retained_bad_page = 0xFF;
|
||||
if (_retained_bad_page < PAGE_COUNT) {
|
||||
// Power management consumes GPREGRET2 during startup. Re-arm this first
|
||||
// strike only after the filesystem has mounted and traversed cleanly. A
|
||||
// later fault on the same page becomes the forced second-strike marker.
|
||||
_boot_scan_requested =
|
||||
setBootScanRequest(BOOT_SCAN_BAD_PAGE_BASE + _retained_bad_page);
|
||||
} else {
|
||||
clearBootScanRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,11 +412,12 @@ int ResilientInternalExtraFS::commitPage() {
|
||||
retirePending(_cached_page);
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
flash_nrf5x_flush();
|
||||
const bool flush_ok = mesh_flash_nrf5x_flush_checked();
|
||||
// Read actual flash, not flash_nrf5x_read(): that API may return its RAM
|
||||
// cache even when the physical erase/program silently failed.
|
||||
if (memcmp(reinterpret_cast<const void*>(address), _page_buffer,
|
||||
PAGE_SIZE) != 0) {
|
||||
if (!flush_ok
|
||||
|| memcmp(reinterpret_cast<const void*>(address), _page_buffer,
|
||||
PAGE_SIZE) != 0) {
|
||||
retirePending(_cached_page);
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@ public:
|
||||
bool scanAndRetireBadPages();
|
||||
bool savePageMap(Adafruit_LittleFS& primary);
|
||||
bool pageMapNeedsSave() const;
|
||||
bool requestBootScan();
|
||||
void resetPageMapForDestructiveRecovery();
|
||||
void requirePageMapRewrite() { _map_repair_needed = true; }
|
||||
bool requestBootScan(bool force_full_scan = false);
|
||||
void acknowledgeRecoveredBootHint();
|
||||
|
||||
protected:
|
||||
@@ -59,10 +61,13 @@ private:
|
||||
mesh::storage::INTERNAL_EXTRAFS_PAGE_MASK;
|
||||
static constexpr uint32_t SCAN_REQUEST = 1UL << 31;
|
||||
static constexpr uint8_t MAX_BAD_PAGES = 4;
|
||||
// GPREGRET2 is retained across a software reset. These values are distinct
|
||||
// from the OTA/bootloader handoff and power-management reason codes.
|
||||
static constexpr uint8_t BOOT_SCAN_ALL = 0x9F;
|
||||
static constexpr uint8_t BOOT_SCAN_BAD_PAGE_BASE = 0x80;
|
||||
// GPREGRET2 is retained across a software reset. Reserve 0x20..0x38 for
|
||||
// the 25 physical ExtraFS pages and 0x3E/0x3F for aggregate scans. This is
|
||||
// deliberately disjoint from OTA staging values, legacy apply results
|
||||
// 0x90..0x9F and 0xB1..0xBF, and bootloader-update results 0xC1..0xC9.
|
||||
static constexpr uint8_t BOOT_SCAN_ALL = 0x3F;
|
||||
static constexpr uint8_t BOOT_SCAN_REPEAT = 0x3E;
|
||||
static constexpr uint8_t BOOT_SCAN_BAD_PAGE_BASE = 0x20;
|
||||
|
||||
uint32_t _bad_pages = 0;
|
||||
uint32_t _pending_pages = 0;
|
||||
@@ -72,6 +77,8 @@ private:
|
||||
uint8_t _retained_bad_page = 0xFF;
|
||||
uint32_t _recorded_bad_pages = 0;
|
||||
uint32_t _recorded_pending_pages = 0;
|
||||
uint32_t _map_generation = 0;
|
||||
bool _map_repair_needed = false;
|
||||
uint32_t _cached_page = INVALID_PAGE;
|
||||
bool _dirty = false;
|
||||
bool _io_failed = false;
|
||||
|
||||
@@ -183,15 +183,8 @@ static bool isNetworkTerminalActive();
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <flash/flash_nrf5x.h>
|
||||
#include <helpers/nrf52/InternalSecondaryFsRepair.h>
|
||||
#if defined(NRF52840_XXAA)
|
||||
static const uint32_t INTERNAL_PRIMARY_FS_START = 0x000ED000UL;
|
||||
#else
|
||||
static const uint32_t INTERNAL_PRIMARY_FS_START = 0x0006D000UL;
|
||||
#endif
|
||||
static const uint32_t INTERNAL_PRIMARY_FS_SIZE =
|
||||
7UL * FLASH_NRF52_PAGE_SIZE;
|
||||
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#endif
|
||||
#if defined(QSPIFLASH)
|
||||
#include <CustomLFS_QSPIFlash.h>
|
||||
@@ -2793,43 +2786,30 @@ void setup() {
|
||||
// InternalFileSystem::begin() auto-formats the entire primary store after a
|
||||
// mount failure. The base mount plus a full raw scan distinguishes a virgin
|
||||
// erased device from nonblank/corrupt identity storage. Only proven-erased
|
||||
// media may be initialized automatically; format is followed by a required
|
||||
// base remount because Adafruit_LittleFS::format() does not remount an
|
||||
// already-unmounted instance.
|
||||
// media may be initialized automatically. A nonblank failure gets three
|
||||
// additional mounts three seconds apart before the unusable store is erased
|
||||
// so the node can still recover without physical access.
|
||||
const mesh::storage::InternalSecondaryFsBootResult primary_fs_boot =
|
||||
mesh::storage::prepareInternalSecondaryFilesystem(
|
||||
[]() -> bool {
|
||||
mesh::storage::beginInternalPrimaryFilesystemSafely(
|
||||
InternalFS,
|
||||
#if defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
return InternalFS.Adafruit_LittleFS::begin(
|
||||
ResilientInternalExtraFS::primaryConfig());
|
||||
ResilientInternalExtraFS::primaryConfig()
|
||||
#else
|
||||
return InternalFS.Adafruit_LittleFS::begin();
|
||||
nullptr
|
||||
#endif
|
||||
},
|
||||
[]() -> bool {
|
||||
return mesh::storage::isErasedFlashRange(
|
||||
INTERNAL_PRIMARY_FS_START, INTERNAL_PRIMARY_FS_SIZE,
|
||||
[](uint32_t address) -> uint32_t {
|
||||
return *reinterpret_cast<const volatile uint32_t*>(address);
|
||||
});
|
||||
},
|
||||
[]() -> bool {
|
||||
InternalFS.end();
|
||||
return InternalFS.format();
|
||||
});
|
||||
if (primary_fs_boot
|
||||
== mesh::storage::InternalSecondaryFsBootResult::PreservedNonBlank
|
||||
|| primary_fs_boot
|
||||
== mesh::storage::InternalSecondaryFsBootResult::InitializationFailed) {
|
||||
if (primary_fs_boot
|
||||
== mesh::storage::InternalSecondaryFsBootResult::PreservedNonBlank) {
|
||||
);
|
||||
if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs =
|
||||
mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs != nullptr) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"InternalFS: mount failed; preserving nonblank primary storage and blocking startup writes");
|
||||
"InternalFS: physical storage unusable; running from volatile RAM filesystem");
|
||||
store.useVolatilePrimaryFS(ram_primary_fs->filesystem());
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"InternalFS: erased primary storage initialization failed; blocking startup writes");
|
||||
"InternalFS: physical storage and RAM fallback initialization failed");
|
||||
store.markPrimaryFSUnavailable();
|
||||
}
|
||||
store.markPrimaryFSUnavailable();
|
||||
}
|
||||
#else
|
||||
InternalFS.begin();
|
||||
@@ -2854,8 +2834,8 @@ void setup() {
|
||||
#else
|
||||
#if defined(EXTRAFS)
|
||||
const bool extra_fs_map_ready =
|
||||
(primary_fs_boot == mesh::storage::InternalSecondaryFsBootResult::Mounted
|
||||
|| primary_fs_boot == mesh::storage::InternalSecondaryFsBootResult::InitializedBlank)
|
||||
!store.isVolatilePrimaryFS()
|
||||
&& mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)
|
||||
&& ExtraFS.loadPageMap(InternalFS);
|
||||
const bool extra_fs_geometry_valid =
|
||||
mesh::storage::isExpectedInternalExtraFsGeometry(
|
||||
@@ -2891,6 +2871,15 @@ void setup() {
|
||||
,
|
||||
radio_available
|
||||
);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (store.isVolatilePrimaryFS()) {
|
||||
strncpy(the_mesh.getNodePrefs()->node_name,
|
||||
mesh::storage::BAD_FILESYSTEM_NODE_NAME,
|
||||
sizeof(the_mesh.getNodePrefs()->node_name));
|
||||
the_mesh.getNodePrefs()->node_name[
|
||||
sizeof(the_mesh.getNodePrefs()->node_name) - 1] = 0;
|
||||
}
|
||||
#endif
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
LittleFS.begin();
|
||||
store.begin();
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
@@ -39,7 +41,26 @@ void halt() {
|
||||
}
|
||||
|
||||
void loadOrCreateIdentity() {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
bool volatile_primary_fs = false;
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr;
|
||||
FILESYSTEM* identity_fs = nullptr;
|
||||
const auto primary_fs_boot =
|
||||
mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS);
|
||||
if (mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
|
||||
identity_fs = &InternalFS;
|
||||
} else {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs == nullptr) {
|
||||
MESH_DEBUG_PRINTLN("InternalFS and RAM fallback initialization failed; rebooting");
|
||||
board.reboot();
|
||||
halt();
|
||||
}
|
||||
identity_fs = &ram_primary_fs->filesystem();
|
||||
volatile_primary_fs = true;
|
||||
}
|
||||
IdentityStore store(*identity_fs, "");
|
||||
#elif defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
IdentityStore store(InternalFS, "");
|
||||
#elif defined(ESP32)
|
||||
@@ -53,9 +74,28 @@ void loadOrCreateIdentity() {
|
||||
#error "Filesystem not defined"
|
||||
#endif
|
||||
|
||||
const bool needs_identity = !store.load("_main", identity)
|
||||
|| mesh::hasReservedIdentityPrefix(identity);
|
||||
bool identity_ready = true;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
IdentityLoadResult identity_load = volatile_primary_fs
|
||||
? store.loadResult("_main", identity)
|
||||
: mesh::storage::loadIdentityWithPrimaryRecovery(
|
||||
InternalFS,
|
||||
[&store]() { return store.loadResult("_main", identity); });
|
||||
if (identity_load == IdentityLoadResult::Unreadable) {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs != nullptr) {
|
||||
identity_fs = &ram_primary_fs->filesystem();
|
||||
store.useFileSystem(*identity_fs);
|
||||
volatile_primary_fs = true;
|
||||
identity_load = IdentityLoadResult::Missing;
|
||||
}
|
||||
}
|
||||
#else
|
||||
const IdentityLoadResult identity_load = store.loadResult("_main", identity);
|
||||
#endif
|
||||
const bool needs_identity = identity_load == IdentityLoadResult::Missing
|
||||
|| (identity_load == IdentityLoadResult::Loaded
|
||||
&& mesh::hasReservedIdentityPrefix(identity));
|
||||
bool identity_ready = identity_load != IdentityLoadResult::Unreadable;
|
||||
if (needs_identity) {
|
||||
identity_ready = mesh::generateUsableLocalIdentity(identity, radio_new_identity);
|
||||
if (identity_ready) identity_ready = store.saveWithRetry("_main", identity);
|
||||
@@ -69,6 +109,12 @@ void loadOrCreateIdentity() {
|
||||
board.reboot();
|
||||
halt(); // Never let setup continue if a platform's reboot returns.
|
||||
}
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (volatile_primary_fs) {
|
||||
MESH_DEBUG_PRINTLN("Node: %s (volatile RAM storage)",
|
||||
mesh::storage::BAD_FILESYSTEM_NODE_NAME);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <helpers/ESP32TrueRandom.h>
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#endif
|
||||
#if defined(ESP32) && MAX_RECENT_REPEATERS > 0
|
||||
#include <new>
|
||||
#endif
|
||||
@@ -155,7 +159,25 @@ void setup() {
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
bool volatile_primary_fs = false;
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr;
|
||||
const auto primary_fs_boot =
|
||||
mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS);
|
||||
if (mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
|
||||
fs = &InternalFS;
|
||||
} else {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs == nullptr) {
|
||||
MESH_DEBUG_PRINTLN("InternalFS and RAM fallback initialization failed; rebooting");
|
||||
board.reboot();
|
||||
return;
|
||||
}
|
||||
fs = &ram_primary_fs->filesystem();
|
||||
volatile_primary_fs = true;
|
||||
}
|
||||
IdentityStore store(*fs, "");
|
||||
#elif defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
fs = &InternalFS;
|
||||
IdentityStore store(InternalFS, "");
|
||||
@@ -171,9 +193,29 @@ void setup() {
|
||||
#else
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
const bool needs_identity = !store.load("_main", the_mesh.self_id)
|
||||
|| mesh::hasReservedIdentityPrefix(the_mesh.self_id);
|
||||
bool identity_ready = true;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
IdentityLoadResult identity_load = volatile_primary_fs
|
||||
? store.loadResult("_main", the_mesh.self_id)
|
||||
: mesh::storage::loadIdentityWithPrimaryRecovery(
|
||||
InternalFS,
|
||||
[&store]() { return store.loadResult("_main", the_mesh.self_id); });
|
||||
if (identity_load == IdentityLoadResult::Unreadable) {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs != nullptr) {
|
||||
fs = &ram_primary_fs->filesystem();
|
||||
store.useFileSystem(*fs);
|
||||
volatile_primary_fs = true;
|
||||
identity_load = IdentityLoadResult::Missing;
|
||||
}
|
||||
}
|
||||
#else
|
||||
const IdentityLoadResult identity_load =
|
||||
store.loadResult("_main", the_mesh.self_id);
|
||||
#endif
|
||||
const bool needs_identity = identity_load == IdentityLoadResult::Missing
|
||||
|| (identity_load == IdentityLoadResult::Loaded
|
||||
&& mesh::hasReservedIdentityPrefix(the_mesh.self_id));
|
||||
bool identity_ready = identity_load != IdentityLoadResult::Unreadable;
|
||||
if (needs_identity) {
|
||||
MESH_DEBUG_PRINTLN("Generating new keypair");
|
||||
identity_ready = mesh::generateUsableLocalIdentity(the_mesh.self_id, radio_new_identity);
|
||||
@@ -218,6 +260,16 @@ void setup() {
|
||||
|
||||
the_mesh.begin(fs);
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (volatile_primary_fs) {
|
||||
strncpy(the_mesh.getNodePrefs()->node_name,
|
||||
mesh::storage::BAD_FILESYSTEM_NODE_NAME,
|
||||
sizeof(the_mesh.getNodePrefs()->node_name));
|
||||
the_mesh.getNodePrefs()->node_name[
|
||||
sizeof(the_mesh.getNodePrefs()->node_name) - 1] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display_ready) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <helpers/ESP32TrueRandom.h>
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#endif
|
||||
|
||||
#ifdef ETHERNET_ENABLED
|
||||
#define ETHERNET_CLI_BANNER "MeshCore Room Server CLI"
|
||||
@@ -83,9 +87,23 @@ void setup() {
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
InternalFS.begin();
|
||||
fs = &InternalFS;
|
||||
IdentityStore store(InternalFS, "");
|
||||
bool volatile_primary_fs = false;
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr;
|
||||
const auto primary_fs_boot =
|
||||
mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS);
|
||||
if (mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
|
||||
fs = &InternalFS;
|
||||
} else {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs == nullptr) {
|
||||
MESH_DEBUG_PRINTLN("InternalFS and RAM fallback initialization failed; rebooting");
|
||||
board.reboot();
|
||||
return;
|
||||
}
|
||||
fs = &ram_primary_fs->filesystem();
|
||||
volatile_primary_fs = true;
|
||||
}
|
||||
IdentityStore store(*fs, "");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
LittleFS.begin();
|
||||
fs = &LittleFS;
|
||||
@@ -98,9 +116,29 @@ void setup() {
|
||||
#else
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
const bool needs_identity = !store.load("_main", the_mesh.self_id)
|
||||
|| mesh::hasReservedIdentityPrefix(the_mesh.self_id);
|
||||
bool identity_ready = true;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
IdentityLoadResult identity_load = volatile_primary_fs
|
||||
? store.loadResult("_main", the_mesh.self_id)
|
||||
: mesh::storage::loadIdentityWithPrimaryRecovery(
|
||||
InternalFS,
|
||||
[&store]() { return store.loadResult("_main", the_mesh.self_id); });
|
||||
if (identity_load == IdentityLoadResult::Unreadable) {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs != nullptr) {
|
||||
fs = &ram_primary_fs->filesystem();
|
||||
store.useFileSystem(*fs);
|
||||
volatile_primary_fs = true;
|
||||
identity_load = IdentityLoadResult::Missing;
|
||||
}
|
||||
}
|
||||
#else
|
||||
const IdentityLoadResult identity_load =
|
||||
store.loadResult("_main", the_mesh.self_id);
|
||||
#endif
|
||||
const bool needs_identity = identity_load == IdentityLoadResult::Missing
|
||||
|| (identity_load == IdentityLoadResult::Loaded
|
||||
&& mesh::hasReservedIdentityPrefix(the_mesh.self_id));
|
||||
bool identity_ready = identity_load != IdentityLoadResult::Unreadable;
|
||||
if (needs_identity) {
|
||||
identity_ready = mesh::generateUsableLocalIdentity(the_mesh.self_id, radio_new_identity);
|
||||
if (identity_ready) identity_ready = store.saveWithRetry("_main", the_mesh.self_id);
|
||||
@@ -128,6 +166,16 @@ void setup() {
|
||||
|
||||
the_mesh.begin(fs);
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (volatile_primary_fs) {
|
||||
strncpy(the_mesh.getNodePrefs()->node_name,
|
||||
mesh::storage::BAD_FILESYSTEM_NODE_NAME,
|
||||
sizeof(the_mesh.getNodePrefs()->node_name));
|
||||
the_mesh.getNodePrefs()->node_name[
|
||||
sizeof(the_mesh.getNodePrefs()->node_name) - 1] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display_ready) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
@@ -316,11 +318,10 @@ public:
|
||||
float getFreqPref() const { return _prefs.freq; }
|
||||
int8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
|
||||
|
||||
void begin(FILESYSTEM& fs) {
|
||||
void begin(FILESYSTEM& fs, bool volatile_primary_fs = false) {
|
||||
_fs = &fs;
|
||||
|
||||
BaseChatMesh::begin();
|
||||
_radio_profiles.begin(_fs, _radio, getRTCClock());
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
IdentityStore store(fs, "");
|
||||
@@ -330,15 +331,44 @@ public:
|
||||
#else
|
||||
IdentityStore store(fs, "/identity");
|
||||
#endif
|
||||
const bool needs_identity = !store.load("_main", self_id, _prefs.node_name, sizeof(_prefs.node_name))
|
||||
|| mesh::hasReservedIdentityPrefix(self_id); // legacy: node_name was from identity file
|
||||
bool identity_ready = true;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
IdentityLoadResult identity_load = volatile_primary_fs
|
||||
? store.loadResult("_main", self_id, _prefs.node_name,
|
||||
sizeof(_prefs.node_name))
|
||||
: mesh::storage::loadIdentityWithPrimaryRecovery(
|
||||
InternalFS, [this, &store]() {
|
||||
return store.loadResult("_main", self_id, _prefs.node_name,
|
||||
sizeof(_prefs.node_name));
|
||||
});
|
||||
if (identity_load == IdentityLoadResult::Unreadable) {
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs =
|
||||
mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs != nullptr) {
|
||||
_fs = &ram_primary_fs->filesystem();
|
||||
store.useFileSystem(*_fs);
|
||||
volatile_primary_fs = true;
|
||||
identity_load = IdentityLoadResult::Missing;
|
||||
}
|
||||
}
|
||||
#else
|
||||
const IdentityLoadResult identity_load = store.loadResult(
|
||||
"_main", self_id, _prefs.node_name, sizeof(_prefs.node_name));
|
||||
#endif
|
||||
const bool needs_identity = identity_load == IdentityLoadResult::Missing
|
||||
|| (identity_load == IdentityLoadResult::Loaded
|
||||
&& mesh::hasReservedIdentityPrefix(self_id)); // legacy: node_name was from identity file
|
||||
bool identity_ready = identity_load != IdentityLoadResult::Unreadable;
|
||||
if (needs_identity) {
|
||||
// Need way to get some entropy to seed RNG
|
||||
Serial.println("Press ENTER to generate key:");
|
||||
char c = 0;
|
||||
while (c != '\n') { // wait for ENTER to be pressed
|
||||
if (Serial.available()) c = Serial.read();
|
||||
if (!volatile_primary_fs) {
|
||||
Serial.println("Press ENTER to generate key:");
|
||||
char c = 0;
|
||||
while (c != '\n') { // wait for ENTER to be pressed
|
||||
if (Serial.available()) c = Serial.read();
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"Physical filesystem unavailable; generating volatile recovery identity");
|
||||
}
|
||||
((StdRNG *)getRNG())->begin(millis());
|
||||
|
||||
@@ -360,6 +390,16 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (volatile_primary_fs) {
|
||||
strncpy(_prefs.node_name, mesh::storage::BAD_FILESYSTEM_NODE_NAME,
|
||||
sizeof(_prefs.node_name));
|
||||
_prefs.node_name[sizeof(_prefs.node_name) - 1] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
_radio_profiles.begin(_fs, _radio, getRTCClock());
|
||||
|
||||
// load persisted prefs
|
||||
if (_fs->exists("/node_prefs")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
@@ -705,8 +745,22 @@ void setup() {
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
InternalFS.begin();
|
||||
the_mesh.begin(InternalFS);
|
||||
FILESYSTEM* primary_fs = &InternalFS;
|
||||
bool volatile_primary_fs = false;
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr;
|
||||
const auto primary_fs_boot =
|
||||
mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS);
|
||||
if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs == nullptr) {
|
||||
MESH_DEBUG_PRINTLN("InternalFS and RAM fallback initialization failed; rebooting");
|
||||
board.reboot();
|
||||
return;
|
||||
}
|
||||
primary_fs = &ram_primary_fs->filesystem();
|
||||
volatile_primary_fs = true;
|
||||
}
|
||||
the_mesh.begin(*primary_fs, volatile_primary_fs);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
LittleFS.begin();
|
||||
the_mesh.begin(LittleFS);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "SensorMesh.h"
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
|
||||
#include <helpers/nrf52/RamFallbackFileSystem.h>
|
||||
#endif
|
||||
#include <helpers/UsbLogging.h>
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -98,7 +102,25 @@ void setup() {
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
bool volatile_primary_fs = false;
|
||||
mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr;
|
||||
const auto primary_fs_boot =
|
||||
mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS);
|
||||
if (mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
|
||||
fs = &InternalFS;
|
||||
} else {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs == nullptr) {
|
||||
MESH_DEBUG_PRINTLN("InternalFS and RAM fallback initialization failed; rebooting");
|
||||
board.reboot();
|
||||
return;
|
||||
}
|
||||
fs = &ram_primary_fs->filesystem();
|
||||
volatile_primary_fs = true;
|
||||
}
|
||||
IdentityStore store(*fs, "");
|
||||
#elif defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
fs = &InternalFS;
|
||||
IdentityStore store(InternalFS, "");
|
||||
@@ -114,9 +136,29 @@ void setup() {
|
||||
#else
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
const bool needs_identity = !store.load("_main", the_mesh.self_id)
|
||||
|| mesh::hasReservedIdentityPrefix(the_mesh.self_id);
|
||||
bool identity_ready = true;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
IdentityLoadResult identity_load = volatile_primary_fs
|
||||
? store.loadResult("_main", the_mesh.self_id)
|
||||
: mesh::storage::loadIdentityWithPrimaryRecovery(
|
||||
InternalFS,
|
||||
[&store]() { return store.loadResult("_main", the_mesh.self_id); });
|
||||
if (identity_load == IdentityLoadResult::Unreadable) {
|
||||
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
|
||||
if (ram_primary_fs != nullptr) {
|
||||
fs = &ram_primary_fs->filesystem();
|
||||
store.useFileSystem(*fs);
|
||||
volatile_primary_fs = true;
|
||||
identity_load = IdentityLoadResult::Missing;
|
||||
}
|
||||
}
|
||||
#else
|
||||
const IdentityLoadResult identity_load =
|
||||
store.loadResult("_main", the_mesh.self_id);
|
||||
#endif
|
||||
const bool needs_identity = identity_load == IdentityLoadResult::Missing
|
||||
|| (identity_load == IdentityLoadResult::Loaded
|
||||
&& mesh::hasReservedIdentityPrefix(the_mesh.self_id));
|
||||
bool identity_ready = identity_load != IdentityLoadResult::Unreadable;
|
||||
if (needs_identity) {
|
||||
MESH_DEBUG_PRINTLN("Generating new keypair");
|
||||
identity_ready = mesh::generateUsableLocalIdentity(the_mesh.self_id, radio_new_identity);
|
||||
@@ -143,6 +185,16 @@ void setup() {
|
||||
|
||||
the_mesh.begin(fs);
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (volatile_primary_fs) {
|
||||
strncpy(the_mesh.getNodePrefs()->node_name,
|
||||
mesh::storage::BAD_FILESYSTEM_NODE_NAME,
|
||||
sizeof(the_mesh.getNodePrefs()->node_name));
|
||||
the_mesh.getNodePrefs()->node_name[
|
||||
sizeof(the_mesh.getNodePrefs()->node_name) - 1] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
||||
#endif
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Build-local fix for the nRF52 InternalFS SoftDevice flash wait.
|
||||
"""Build-local reliability fixes for the nRF52 InternalFS flash backend.
|
||||
|
||||
GCC 14/LTO can treat the naked SVC wrapper as not writing through its pointer
|
||||
argument. It then folds wait_for_async_flash_op_completion() into an immediate
|
||||
success return, allowing LittleFS to read or rewrite a page before the async
|
||||
erase/program has completed. Keep the SoftDevice state volatile and add a
|
||||
compiler barrier after the SVC call. Do not modify PlatformIO's shared SDK.
|
||||
success return. The framework cache also discards erase/program failures and
|
||||
InternalFS always reports sync success. Patch private build copies so completion
|
||||
is observed, physical readback is volatile, and LittleFS receives LFS_ERR_IO.
|
||||
Do not modify PlatformIO's shared SDK.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
@@ -22,36 +23,197 @@ FIXED_WAIT = """ volatile uint8_t sd_en = 0;
|
||||
if (sd_en) {
|
||||
"""
|
||||
|
||||
OLD_FLUSH = """void flash_nrf5x_flush (void)
|
||||
{
|
||||
flash_cache_flush(&_cache);
|
||||
}
|
||||
"""
|
||||
FIXED_FLUSH = """extern bool mesh_flash_cache_take_flush_result(void);
|
||||
|
||||
def patched_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
if FIXED_WAIT in source and OLD_WAIT not in source:
|
||||
bool mesh_flash_nrf5x_flush_checked (void)
|
||||
{
|
||||
flash_cache_flush(&_cache);
|
||||
return mesh_flash_cache_take_flush_result();
|
||||
}
|
||||
|
||||
void flash_nrf5x_flush (void)
|
||||
{
|
||||
(void) mesh_flash_nrf5x_flush_checked();
|
||||
}
|
||||
"""
|
||||
|
||||
OLD_VERIFY = """static bool fal_verify (uint32_t addr, void const * buf, uint32_t len)
|
||||
{
|
||||
return 0 == memcmp((void*) addr, buf, len);
|
||||
}
|
||||
"""
|
||||
FIXED_VERIFY = """static bool fal_verify (uint32_t addr, void const * buf, uint32_t len)
|
||||
{
|
||||
volatile uint8_t const * flash = (volatile uint8_t const *) addr;
|
||||
uint8_t const * expected = (uint8_t const *) buf;
|
||||
for (uint32_t i = 0; i < len; ++i) {
|
||||
if (flash[i] != expected[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
"""
|
||||
|
||||
OLD_CACHE_FLUSH = """void flash_cache_flush (flash_cache_t* fc)
|
||||
{
|
||||
if ( fc->cache_addr == FLASH_CACHE_INVALID_ADDR ) return;
|
||||
|
||||
// skip erase & program if verify() exists, and memory matches
|
||||
if ( !(fc->verify && fc->verify(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE)) )
|
||||
{
|
||||
// indicator TODO allow to disable flash indicator
|
||||
ledOn(LED_BUILTIN);
|
||||
|
||||
fc->erase(fc->cache_addr);
|
||||
fc->program(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE);
|
||||
|
||||
ledOff(LED_BUILTIN);
|
||||
}
|
||||
|
||||
fc->cache_addr = FLASH_CACHE_INVALID_ADDR;
|
||||
}
|
||||
"""
|
||||
FIXED_CACHE_FLUSH = """static bool mesh_flash_cache_flush_ok = true;
|
||||
|
||||
bool mesh_flash_cache_take_flush_result(void)
|
||||
{
|
||||
bool const result = mesh_flash_cache_flush_ok;
|
||||
mesh_flash_cache_flush_ok = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
void flash_cache_flush (flash_cache_t* fc)
|
||||
{
|
||||
if ( fc->cache_addr == FLASH_CACHE_INVALID_ADDR ) return;
|
||||
|
||||
// skip erase & program if verify() exists, and memory matches
|
||||
if ( !(fc->verify && fc->verify(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE)) )
|
||||
{
|
||||
// indicator TODO allow to disable flash indicator
|
||||
ledOn(LED_BUILTIN);
|
||||
|
||||
bool ok = fc->erase(fc->cache_addr);
|
||||
if ( ok ) ok = fc->program(fc->cache_addr, fc->cache_buf,
|
||||
FLASH_CACHE_SIZE) == FLASH_CACHE_SIZE;
|
||||
if ( ok && fc->verify ) {
|
||||
ok = fc->verify(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE);
|
||||
}
|
||||
if ( !ok ) mesh_flash_cache_flush_ok = false;
|
||||
|
||||
ledOff(LED_BUILTIN);
|
||||
}
|
||||
|
||||
fc->cache_addr = FLASH_CACHE_INVALID_ADDR;
|
||||
}
|
||||
"""
|
||||
|
||||
OLD_INTERNAL_READ = """ VERIFY( flash_nrf5x_read(buffer, addr, size) > 0, -1);
|
||||
"""
|
||||
FIXED_INTERNAL_READ = """ VERIFY( flash_nrf5x_read(buffer, addr, size) == (int) size, LFS_ERR_IO);
|
||||
"""
|
||||
OLD_INTERNAL_PROG = """ VERIFY( flash_nrf5x_write(addr, buffer, size), -1)
|
||||
"""
|
||||
FIXED_INTERNAL_PROG = """ VERIFY( flash_nrf5x_write(addr, buffer, size) == (int) size, LFS_ERR_IO)
|
||||
"""
|
||||
OLD_INTERNAL_ERASE_WRITE = """ flash_nrf5x_write8(addr + i, 0xFF);
|
||||
"""
|
||||
FIXED_INTERNAL_ERASE_WRITE = """ VERIFY( flash_nrf5x_write8(addr + i, 0xFF) == 1, LFS_ERR_IO);
|
||||
"""
|
||||
OLD_INTERNAL_SYNC = """static int _internal_flash_sync (const struct lfs_config *c)
|
||||
{
|
||||
(void) c;
|
||||
flash_nrf5x_flush();
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
FIXED_INTERNAL_SYNC = """extern \"C\" bool mesh_flash_nrf5x_flush_checked(void);
|
||||
|
||||
static int _internal_flash_sync (const struct lfs_config *c)
|
||||
{
|
||||
(void) c;
|
||||
return mesh_flash_nrf5x_flush_checked() ? 0 : LFS_ERR_IO;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _replace_once(source, old, new, description):
|
||||
if new in source and old not in source:
|
||||
return source
|
||||
if source.count(OLD_WAIT) != 1:
|
||||
if source.count(old) != 1:
|
||||
raise RuntimeError(
|
||||
"nRF52 flash fix: unrecognized InternalFS flash driver; review "
|
||||
"the framework update before building (shared SDK not modified)"
|
||||
"nRF52 flash fix: unrecognized " + description + "; review the "
|
||||
"framework update before building (shared SDK not modified)"
|
||||
)
|
||||
return source.replace(OLD_WAIT, FIXED_WAIT)
|
||||
return source.replace(old, new)
|
||||
|
||||
|
||||
def replace_driver(build_env, node):
|
||||
def patched_driver_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
source = _replace_once(source, OLD_WAIT, FIXED_WAIT, "InternalFS flash driver")
|
||||
source = _replace_once(source, OLD_FLUSH, FIXED_FLUSH, "InternalFS flush API")
|
||||
return _replace_once(source, OLD_VERIFY, FIXED_VERIFY,
|
||||
"InternalFS physical verification")
|
||||
|
||||
|
||||
def patched_cache_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
return _replace_once(source, OLD_CACHE_FLUSH, FIXED_CACHE_FLUSH,
|
||||
"InternalFS flash cache")
|
||||
|
||||
|
||||
def patched_internal_fs_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
source = _replace_once(source, OLD_INTERNAL_READ, FIXED_INTERNAL_READ,
|
||||
"InternalFS read callback")
|
||||
source = _replace_once(source, OLD_INTERNAL_PROG, FIXED_INTERNAL_PROG,
|
||||
"InternalFS program callback")
|
||||
source = _replace_once(source, OLD_INTERNAL_ERASE_WRITE,
|
||||
FIXED_INTERNAL_ERASE_WRITE,
|
||||
"InternalFS erase callback")
|
||||
return _replace_once(source, OLD_INTERNAL_SYNC, FIXED_INTERNAL_SYNC,
|
||||
"InternalFS sync callback")
|
||||
|
||||
|
||||
# Backward-compatible name used by the regression test and any local tooling.
|
||||
patched_source = patched_driver_source
|
||||
|
||||
|
||||
def replace_framework_source(build_env, node):
|
||||
source = Path(node.srcnode().get_abspath())
|
||||
patched = patched_source(source.read_text(encoding="utf-8"))
|
||||
patcher = {
|
||||
"flash_nrf5x.c": patched_driver_source,
|
||||
"flash_cache.c": patched_cache_source,
|
||||
"InternalFileSystem.cpp": patched_internal_fs_source,
|
||||
}.get(source.name)
|
||||
if patcher is None:
|
||||
raise RuntimeError("nRF52 flash fix: unexpected source " + source.name)
|
||||
patched = patcher(source.read_text(encoding="utf-8"))
|
||||
build_env.AppendUnique(CPPPATH=[str(source.parent)])
|
||||
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-flash" / source.name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not destination.exists() or destination.read_text(encoding="utf-8") != patched:
|
||||
destination.write_text(patched, encoding="utf-8")
|
||||
print("nRF52 flash: build-local SoftDevice completion wait fix enabled")
|
||||
print("nRF52 flash: build-local verified flush fix enabled for " + source.name)
|
||||
return build_env.File(str(destination))
|
||||
|
||||
|
||||
def install(build_env):
|
||||
build_env.AddBuildMiddleware(
|
||||
replace_driver,
|
||||
replace_framework_source,
|
||||
"*flash_nrf5x.c",
|
||||
)
|
||||
build_env.AddBuildMiddleware(
|
||||
replace_framework_source,
|
||||
"*flash_cache.c",
|
||||
)
|
||||
build_env.AddBuildMiddleware(
|
||||
replace_framework_source,
|
||||
"*InternalFileSystem.cpp",
|
||||
)
|
||||
|
||||
|
||||
if "Import" in globals():
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace mesh {
|
||||
*/
|
||||
class AtomicFileWriter {
|
||||
static const size_t TEMP_PATH_CAPACITY = 96;
|
||||
static const uint8_t VALIDATION_ATTEMPTS = 3;
|
||||
|
||||
FILESYSTEM* _fs;
|
||||
const char* _target_path;
|
||||
@@ -31,7 +32,7 @@ class AtomicFileWriter {
|
||||
|
||||
static uint32_t updateCRC32(uint32_t crc, const uint8_t* data, size_t len);
|
||||
void removeTempFile();
|
||||
bool validateTempFile();
|
||||
bool validateFile(const char* path);
|
||||
|
||||
public:
|
||||
AtomicFileWriter(FILESYSTEM* fs, const char* target_path);
|
||||
@@ -76,28 +77,31 @@ void AtomicFileWriter::removeTempFile() {
|
||||
}
|
||||
}
|
||||
|
||||
bool AtomicFileWriter::validateTempFile() {
|
||||
File verify(*_fs);
|
||||
if (!verify.open(_temp_path, FILE_O_READ)) return false;
|
||||
bool AtomicFileWriter::validateFile(const char* path) {
|
||||
for (uint8_t attempt = 0; attempt < VALIDATION_ATTEMPTS; ++attempt) {
|
||||
File verify(*_fs);
|
||||
if (!verify.open(path, FILE_O_READ)) continue;
|
||||
|
||||
bool valid = verify.size() == _bytes_written;
|
||||
uint32_t read_crc = 0xFFFFFFFFUL;
|
||||
size_t remaining = _bytes_written;
|
||||
uint8_t buf[64];
|
||||
bool valid = verify.size() == _bytes_written;
|
||||
uint32_t read_crc = 0xFFFFFFFFUL;
|
||||
size_t remaining = _bytes_written;
|
||||
uint8_t buf[64];
|
||||
|
||||
while (valid && remaining > 0) {
|
||||
size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
|
||||
int count = verify.read(buf, (uint16_t)chunk);
|
||||
if (count != (int)chunk) {
|
||||
valid = false;
|
||||
break;
|
||||
while (valid && remaining > 0) {
|
||||
size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
|
||||
int count = verify.read(buf, (uint16_t)chunk);
|
||||
if (count != (int)chunk) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
read_crc = updateCRC32(read_crc, buf, chunk);
|
||||
remaining -= chunk;
|
||||
}
|
||||
read_crc = updateCRC32(read_crc, buf, chunk);
|
||||
remaining -= chunk;
|
||||
}
|
||||
|
||||
verify.close();
|
||||
return valid && remaining == 0 && read_crc == _crc;
|
||||
verify.close();
|
||||
if (valid && remaining == 0 && read_crc == _crc) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
AtomicFileWriter::AtomicFileWriter(FILESYSTEM* fs, const char* target_path)
|
||||
@@ -166,8 +170,12 @@ bool AtomicFileWriter::commit(bool content_valid) {
|
||||
_opened = false;
|
||||
}
|
||||
|
||||
if (success) success = validateTempFile();
|
||||
if (success) success = validateFile(_temp_path);
|
||||
if (success) success = _fs->rename(_temp_path, _target_path);
|
||||
// A lower layer can acknowledge a metadata update before the physical page
|
||||
// is durable. Validate the public filename too; callers must never clear a
|
||||
// dirty flag or announce success while the old/corrupt file is still live.
|
||||
if (success) success = validateFile(_target_path);
|
||||
if (!success) removeTempFile();
|
||||
|
||||
_finished = true;
|
||||
|
||||
@@ -24,18 +24,30 @@ bool IdentityStore::recover(const char* name) {
|
||||
}
|
||||
|
||||
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
|
||||
return load(name, id, nullptr, 0);
|
||||
return loadResult(name, id) == IdentityLoadResult::Loaded;
|
||||
}
|
||||
|
||||
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz) {
|
||||
return loadResult(name, id, display_name, max_name_sz)
|
||||
== IdentityLoadResult::Loaded;
|
||||
}
|
||||
|
||||
IdentityLoadResult IdentityStore::loadResult(
|
||||
const char *name, mesh::LocalIdentity& id) {
|
||||
return loadResult(name, id, nullptr, 0);
|
||||
}
|
||||
|
||||
IdentityLoadResult IdentityStore::loadResult(
|
||||
const char *name, mesh::LocalIdentity& id, char display_name[],
|
||||
int max_name_sz) {
|
||||
char filename[40];
|
||||
if (snprintf(filename, sizeof(filename), "%s/%s.id", _dir, name)
|
||||
>= (int)sizeof(filename)) return false;
|
||||
>= (int)sizeof(filename)) return IdentityLoadResult::Unreadable;
|
||||
for (unsigned attempt = 0; attempt < IO_ATTEMPTS; ++attempt) {
|
||||
if (!recover(name)) continue;
|
||||
bool present = false;
|
||||
if (!mesh::filePresence(_fs, filename, present)) continue;
|
||||
if (!present) return false;
|
||||
if (!present) return IdentityLoadResult::Missing;
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = _fs->open(filename, "r");
|
||||
#else
|
||||
@@ -62,10 +74,12 @@ bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display
|
||||
}
|
||||
file.close();
|
||||
id = loaded;
|
||||
return true;
|
||||
return IdentityLoadResult::Loaded;
|
||||
}
|
||||
// Callers may provision a replacement after bounded recovery is exhausted.
|
||||
return false;
|
||||
// An existing file which cannot be read is materially different from a
|
||||
// fresh device. Startup callers must preserve it instead of replacing the
|
||||
// radio's private key after a transient or localized flash failure.
|
||||
return IdentityLoadResult::Unreadable;
|
||||
}
|
||||
|
||||
bool IdentityStore::saveWithRetry(const char* name, const mesh::LocalIdentity& id) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(ESP32) || defined(RP2040_PLATFORM)
|
||||
#include <FS.h>
|
||||
#define FILESYSTEM fs::FS
|
||||
@@ -11,18 +13,28 @@
|
||||
#endif
|
||||
#include <Identity.h>
|
||||
|
||||
enum class IdentityLoadResult : uint8_t {
|
||||
Loaded,
|
||||
Missing,
|
||||
Unreadable,
|
||||
};
|
||||
|
||||
class IdentityStore {
|
||||
FILESYSTEM* _fs;
|
||||
const char* _dir;
|
||||
public:
|
||||
static constexpr unsigned IO_ATTEMPTS = 3;
|
||||
IdentityStore(FILESYSTEM& fs, const char* dir): _fs(&fs), _dir(dir) { }
|
||||
void useFileSystem(FILESYSTEM& fs) { _fs = &fs; }
|
||||
|
||||
void begin() {
|
||||
if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
|
||||
// Recover an interrupted non-replacing rename before deciding this is a
|
||||
// fresh identity. False means the previous image must remain protected.
|
||||
bool recover(const char* name);
|
||||
IdentityLoadResult loadResult(const char *name, mesh::LocalIdentity& id);
|
||||
IdentityLoadResult loadResult(const char *name, mesh::LocalIdentity& id,
|
||||
char display_name[], int max_name_sz);
|
||||
bool load(const char *name, mesh::LocalIdentity& id);
|
||||
bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz);
|
||||
bool save(const char *name, const mesh::LocalIdentity& id);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
extern "C" bool mesh_flash_nrf5x_flush_checked(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
#include <InternalFileSystem.h>
|
||||
#include <flash/flash_nrf5x.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/nrf52/InternalSecondaryFsRepair.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace storage {
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
static const uint32_t INTERNAL_PRIMARY_FS_START = 0xED000UL;
|
||||
#else
|
||||
static const uint32_t INTERNAL_PRIMARY_FS_START = 0x6D000UL;
|
||||
#endif
|
||||
static const uint32_t INTERNAL_PRIMARY_FS_SIZE =
|
||||
7UL * FLASH_NRF52_PAGE_SIZE;
|
||||
static const uint8_t INTERNAL_PRIMARY_FS_MOUNT_ATTEMPTS = 3;
|
||||
|
||||
// InternalFileSystem::begin() formats after one failed mount. Identity storage
|
||||
// instead gets several non-destructive mount attempts and is formatted only
|
||||
// when every word in the complete reserved range is still erased.
|
||||
inline InternalSecondaryFsBootResult beginInternalPrimaryFilesystemSafely(
|
||||
InternalFileSystem& fs, struct lfs_config* config = nullptr) {
|
||||
const auto mount_with_retries = [&fs, config]() -> bool {
|
||||
for (uint8_t attempt = 0; attempt < INTERNAL_PRIMARY_FS_MOUNT_ATTEMPTS;
|
||||
++attempt) {
|
||||
if (attempt != 0) fs.end();
|
||||
const bool mounted = config == nullptr
|
||||
? fs.Adafruit_LittleFS::begin()
|
||||
: fs.Adafruit_LittleFS::begin(config);
|
||||
if (mounted) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
InternalSecondaryFsBootResult result = prepareInternalSecondaryFilesystem(
|
||||
mount_with_retries,
|
||||
[]() -> bool {
|
||||
return isErasedFlashRange(
|
||||
INTERNAL_PRIMARY_FS_START, INTERNAL_PRIMARY_FS_SIZE,
|
||||
[](uint32_t address) -> uint32_t {
|
||||
return *reinterpret_cast<const volatile uint32_t*>(address);
|
||||
});
|
||||
},
|
||||
[&fs]() -> bool {
|
||||
fs.end();
|
||||
return fs.format();
|
||||
});
|
||||
if (result == InternalSecondaryFsBootResult::Mounted
|
||||
|| result == InternalSecondaryFsBootResult::InitializedBlank) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// A node which can never leave startup is not useful. Give transient flash
|
||||
// or SoftDevice contention three widely spaced chances to clear before the
|
||||
// explicitly authorized last resort: erase the unusable primary store.
|
||||
for (uint8_t retry = 0; retry < 3; ++retry) {
|
||||
delay(3000);
|
||||
fs.end();
|
||||
const bool mounted = config == nullptr
|
||||
? fs.Adafruit_LittleFS::begin()
|
||||
: fs.Adafruit_LittleFS::begin(config);
|
||||
if (mounted) return InternalSecondaryFsBootResult::Mounted;
|
||||
}
|
||||
|
||||
fs.end();
|
||||
if (!fs.format() || !mount_with_retries()) {
|
||||
return InternalSecondaryFsBootResult::InitializationFailed;
|
||||
}
|
||||
return InternalSecondaryFsBootResult::ReinitializedUnreadable;
|
||||
}
|
||||
|
||||
inline bool internalPrimaryFilesystemReady(
|
||||
InternalSecondaryFsBootResult result) {
|
||||
return result == InternalSecondaryFsBootResult::Mounted
|
||||
|| result == InternalSecondaryFsBootResult::InitializedBlank
|
||||
|| result == InternalSecondaryFsBootResult::ReinitializedUnreadable;
|
||||
}
|
||||
|
||||
template <typename LoadIdentity>
|
||||
IdentityLoadResult loadIdentityWithPrimaryRecovery(
|
||||
InternalFileSystem& fs, LoadIdentity load_identity,
|
||||
struct lfs_config* config = nullptr) {
|
||||
IdentityLoadResult result = load_identity();
|
||||
if (result != IdentityLoadResult::Unreadable) return result;
|
||||
|
||||
for (uint8_t retry = 0; retry < 3; ++retry) {
|
||||
delay(3000);
|
||||
result = load_identity();
|
||||
if (result != IdentityLoadResult::Unreadable) return result;
|
||||
}
|
||||
|
||||
fs.end();
|
||||
if (!fs.format()) return IdentityLoadResult::Unreadable;
|
||||
for (uint8_t attempt = 0; attempt < INTERNAL_PRIMARY_FS_MOUNT_ATTEMPTS;
|
||||
++attempt) {
|
||||
if (attempt != 0) fs.end();
|
||||
const bool mounted = config == nullptr
|
||||
? fs.Adafruit_LittleFS::begin()
|
||||
: fs.Adafruit_LittleFS::begin(config);
|
||||
if (mounted) return load_identity();
|
||||
}
|
||||
return IdentityLoadResult::Unreadable;
|
||||
}
|
||||
|
||||
} // namespace storage
|
||||
} // namespace mesh
|
||||
|
||||
#endif // NRF52_PLATFORM
|
||||
@@ -38,6 +38,7 @@ bool isErasedFlashRange(uint32_t start, uint32_t size, ReadWord read_word) {
|
||||
enum class InternalSecondaryFsBootResult {
|
||||
Mounted,
|
||||
InitializedBlank,
|
||||
ReinitializedUnreadable,
|
||||
PreservedNonBlank,
|
||||
InitializationFailed,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
#include <Adafruit_LittleFS.h>
|
||||
#include <new>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace storage {
|
||||
|
||||
static constexpr char BAD_FILESYSTEM_NODE_NAME[] = "bad file system";
|
||||
|
||||
// Emergency-only volatile storage. It is allocated only after physical
|
||||
// primary flash has failed every retry and a destructive reinitialization.
|
||||
// Eight KiB is enough for identity/preferences and basic node operation while
|
||||
// keeping the normal boot's RAM footprint unchanged.
|
||||
class RamFallbackFileSystem {
|
||||
static constexpr uint32_t BLOCK_SIZE = 128;
|
||||
static constexpr uint32_t STORAGE_SIZE = 8UL * 1024UL;
|
||||
|
||||
alignas(4) uint8_t _storage[STORAGE_SIZE];
|
||||
struct lfs_config _config;
|
||||
Adafruit_LittleFS _filesystem;
|
||||
|
||||
static RamFallbackFileSystem* owner(const struct lfs_config* config) {
|
||||
return static_cast<RamFallbackFileSystem*>(config->context);
|
||||
}
|
||||
|
||||
static bool validRange(lfs_block_t block, lfs_off_t off, lfs_size_t size) {
|
||||
return block < STORAGE_SIZE / BLOCK_SIZE && off <= BLOCK_SIZE
|
||||
&& size <= BLOCK_SIZE - off;
|
||||
}
|
||||
|
||||
static int read(const struct lfs_config* config, lfs_block_t block,
|
||||
lfs_off_t off, void* buffer, lfs_size_t size) {
|
||||
if (!validRange(block, off, size)) return LFS_ERR_IO;
|
||||
memcpy(buffer, owner(config)->_storage + block * BLOCK_SIZE + off, size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int program(const struct lfs_config* config, lfs_block_t block,
|
||||
lfs_off_t off, const void* buffer, lfs_size_t size) {
|
||||
if (!validRange(block, off, size)) return LFS_ERR_IO;
|
||||
uint8_t* destination = owner(config)->_storage + block * BLOCK_SIZE + off;
|
||||
const uint8_t* source = static_cast<const uint8_t*>(buffer);
|
||||
for (lfs_size_t i = 0; i < size; ++i) destination[i] &= source[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int erase(const struct lfs_config* config, lfs_block_t block) {
|
||||
if (!validRange(block, 0, BLOCK_SIZE)) return LFS_ERR_IO;
|
||||
memset(owner(config)->_storage + block * BLOCK_SIZE, 0xFF, BLOCK_SIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sync(const struct lfs_config*) { return 0; }
|
||||
|
||||
public:
|
||||
RamFallbackFileSystem() : _config{}, _filesystem(&_config) {
|
||||
memset(_storage, 0xFF, sizeof(_storage));
|
||||
_config.context = this;
|
||||
_config.read = read;
|
||||
_config.prog = program;
|
||||
_config.erase = erase;
|
||||
_config.sync = sync;
|
||||
_config.read_size = 16;
|
||||
_config.prog_size = 16;
|
||||
_config.block_size = BLOCK_SIZE;
|
||||
_config.block_count = STORAGE_SIZE / BLOCK_SIZE;
|
||||
_config.lookahead = 64;
|
||||
}
|
||||
|
||||
bool begin() {
|
||||
return _filesystem.format() && _filesystem.begin();
|
||||
}
|
||||
|
||||
Adafruit_LittleFS& filesystem() { return _filesystem; }
|
||||
};
|
||||
|
||||
inline RamFallbackFileSystem* createRamFallbackFileSystem() {
|
||||
RamFallbackFileSystem* fallback = new (std::nothrow) RamFallbackFileSystem();
|
||||
if (fallback != nullptr && fallback->begin()) return fallback;
|
||||
delete fallback;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace storage
|
||||
} // namespace mesh
|
||||
|
||||
#endif // NRF52_PLATFORM
|
||||
@@ -36,6 +36,8 @@ public:
|
||||
size_t max_write = std::numeric_limits<size_t>::max();
|
||||
bool fail_read_open = false;
|
||||
bool fail_rename = false;
|
||||
bool false_success_rename = false;
|
||||
unsigned read_open_failures_remaining = 0;
|
||||
unsigned rename_calls = 0;
|
||||
|
||||
bool exists(const char* path) const {
|
||||
@@ -49,6 +51,7 @@ public:
|
||||
bool rename(const char* from, const char* to) {
|
||||
rename_calls++;
|
||||
if (fail_rename) return false;
|
||||
if (false_success_rename) return true;
|
||||
auto source = files.find(from);
|
||||
if (source == files.end()) return false;
|
||||
files[to] = source->second;
|
||||
@@ -69,6 +72,10 @@ bool FakeFile::open(const char* path, uint8_t mode) {
|
||||
_path = path;
|
||||
_write_mode = mode == FILE_O_WRITE;
|
||||
if (!_write_mode && (_fs->fail_read_open || !_fs->exists(path))) return false;
|
||||
if (!_write_mode && _fs->read_open_failures_remaining > 0) {
|
||||
_fs->read_open_failures_remaining--;
|
||||
return false;
|
||||
}
|
||||
if (_write_mode) {
|
||||
auto& contents = _fs->files[_path];
|
||||
_position = contents.size();
|
||||
@@ -188,6 +195,34 @@ TEST(AtomicFileWriter, RenameFailurePreservesLiveFileAndCleansTemp) {
|
||||
EXPECT_EQ(fs.rename_calls, 1u);
|
||||
}
|
||||
|
||||
TEST(AtomicFileWriter, FalseSuccessRenameIsDetectedByLiveReadback) {
|
||||
FakeFilesystem fs;
|
||||
fs.files["/prefs"] = bytes("old");
|
||||
fs.false_success_rename = true;
|
||||
|
||||
mesh::AtomicFileWriter writer(&fs, "/prefs");
|
||||
const uint8_t replacement[] = {'n', 'e', 'w'};
|
||||
ASSERT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement));
|
||||
|
||||
EXPECT_FALSE(writer.commit());
|
||||
EXPECT_EQ(fs.files["/prefs"], bytes("old"));
|
||||
EXPECT_FALSE(fs.exists("/prefs.tmp"));
|
||||
EXPECT_EQ(fs.rename_calls, 1u);
|
||||
}
|
||||
|
||||
TEST(AtomicFileWriter, ValidationRetriesTransientReadOpenFailures) {
|
||||
FakeFilesystem fs;
|
||||
fs.files["/prefs"] = bytes("old");
|
||||
fs.read_open_failures_remaining = 2;
|
||||
|
||||
mesh::AtomicFileWriter writer(&fs, "/prefs");
|
||||
const uint8_t replacement[] = {'n', 'e', 'w'};
|
||||
ASSERT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement));
|
||||
|
||||
EXPECT_TRUE(writer.commit());
|
||||
EXPECT_EQ(fs.files["/prefs"], bytes("new"));
|
||||
}
|
||||
|
||||
TEST(AtomicFileWriter, AbandonedWriteAndStaleTempAreCleaned) {
|
||||
FakeFilesystem fs;
|
||||
fs.files["/prefs"] = bytes("old");
|
||||
|
||||
@@ -56,6 +56,8 @@ class IdentityAndSettingsRecovery(unittest.TestCase):
|
||||
for signature in ('bool IdentityStore::recover(',
|
||||
'bool IdentityStore::load(const char *name, mesh::LocalIdentity& id)',
|
||||
'bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz)',
|
||||
'IdentityLoadResult IdentityStore::loadResult(\n const char *name, mesh::LocalIdentity& id)',
|
||||
'IdentityLoadResult IdentityStore::loadResult(\n const char *name, mesh::LocalIdentity& id, char display_name[]',
|
||||
'bool IdentityStore::saveWithRetry(',
|
||||
'bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id)',
|
||||
'bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char display_name[])'):
|
||||
@@ -87,6 +89,10 @@ int main() {
|
||||
if (display) assert(strcmp(name, "saved name") == 0);
|
||||
} else {
|
||||
assert(live.pub_key[0] == 9 && strcmp(name, "unchanged") == 0);
|
||||
const auto state = display
|
||||
? store.loadResult("main", live, name, sizeof(name))
|
||||
: store.loadResult("main", live);
|
||||
assert(state == IdentityLoadResult::Loaded);
|
||||
assert(store.saveWithRetry("main", live));
|
||||
}
|
||||
}
|
||||
@@ -160,6 +166,7 @@ int main() {
|
||||
with self.subTest(role=role):
|
||||
source = (ROOT / 'examples' / role / 'main.cpp').read_text()
|
||||
self.assertIn('if (identity_ready) identity_ready = store.saveWithRetry(', source)
|
||||
self.assertIn('IdentityLoadResult::Unreadable', source)
|
||||
failed = extract_braced(source, 'if (!identity_ready)')
|
||||
self.assertIn('board.reboot();', failed)
|
||||
self.assertIn('halt();' if role == 'kiss_modem' else 'return;', failed)
|
||||
|
||||
@@ -875,12 +875,12 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
self.assertIn("_identity_creation_blocked = true;", unmounted_branch)
|
||||
self.assertIn("_contact_load_incomplete = true;", unmounted_branch)
|
||||
self.assertIn("return;", unmounted_branch)
|
||||
self.assertIn("_primary_storage_unavailable = true;", primary_failure_branch)
|
||||
self.assertIn("_identity_creation_blocked = true;", primary_failure_branch)
|
||||
self.assertIn("_contact_load_incomplete = true;", primary_failure_branch)
|
||||
self.assertIn("return;", primary_failure_branch)
|
||||
self.assertGreaterEqual(primary_failure_branch.count("delay(3000);"), 2)
|
||||
self.assertIn("_fs->format()", primary_failure_branch)
|
||||
self.assertIn("createRamFallbackFileSystem()", primary_failure_branch)
|
||||
self.assertIn("useVolatilePrimaryFS", primary_failure_branch)
|
||||
self.assertNotIn("recoverPrimaryFilesystem", store_begin)
|
||||
self.assertNotIn("->format(", store_begin)
|
||||
self.assertIn("IdentityLoadResult::Unreadable", store_begin)
|
||||
secondary_failure = store_begin.index(
|
||||
"if (_fsExtra != nullptr && !validateLfsFilesystem(_fsExtra))"
|
||||
)
|
||||
@@ -905,46 +905,36 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
|
||||
setup = function_body(MAIN.read_text(encoding="utf-8"), "void setup()")
|
||||
main = MAIN.read_text(encoding="utf-8")
|
||||
primary_helper = (
|
||||
ROOT / "src/helpers/nrf52/InternalPrimaryFsBoot.h"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"static const uint32_t INTERNAL_PRIMARY_FS_START = 0x000ED000UL;",
|
||||
main,
|
||||
"static const uint32_t INTERNAL_PRIMARY_FS_START = 0xED000UL;",
|
||||
primary_helper,
|
||||
)
|
||||
self.assertIn(
|
||||
"static const uint32_t INTERNAL_PRIMARY_FS_START = 0x0006D000UL;",
|
||||
main,
|
||||
"static const uint32_t INTERNAL_PRIMARY_FS_START = 0x6D000UL;",
|
||||
primary_helper,
|
||||
)
|
||||
self.assertIn("7UL * FLASH_NRF52_PAGE_SIZE", main)
|
||||
self.assertIn("7UL * FLASH_NRF52_PAGE_SIZE", primary_helper)
|
||||
primary_policy = setup.index(
|
||||
"InternalSecondaryFsBootResult primary_fs_boot"
|
||||
)
|
||||
primary_mount = setup.index(
|
||||
"InternalFS.Adafruit_LittleFS::begin()", primary_policy
|
||||
)
|
||||
blank_scan = setup.index("isErasedFlashRange(", primary_mount)
|
||||
primary_format = setup.index("InternalFS.format()", blank_scan)
|
||||
quarantine = setup.index("store.markPrimaryFSUnavailable()", primary_format)
|
||||
store_begin = setup.index("store.begin()", quarantine)
|
||||
self.assertLess(primary_policy, primary_mount)
|
||||
self.assertLess(primary_mount, blank_scan)
|
||||
self.assertLess(blank_scan, primary_format)
|
||||
self.assertLess(primary_format, quarantine)
|
||||
self.assertLess(quarantine, store_begin)
|
||||
primary_policy_body = setup[primary_policy:quarantine]
|
||||
self.assertIn("INTERNAL_PRIMARY_FS_START", primary_policy_body)
|
||||
self.assertIn("INTERNAL_PRIMARY_FS_SIZE", primary_policy_body)
|
||||
self.assertIn(
|
||||
"InternalSecondaryFsBootResult::PreservedNonBlank",
|
||||
primary_policy_body,
|
||||
)
|
||||
self.assertIn(
|
||||
"InternalSecondaryFsBootResult::InitializationFailed",
|
||||
primary_policy_body,
|
||||
)
|
||||
fallback = setup.index("createRamFallbackFileSystem()", primary_policy)
|
||||
volatile_store = setup.index("store.useVolatilePrimaryFS", fallback)
|
||||
store_begin = setup.index("store.begin()", volatile_store)
|
||||
self.assertLess(primary_policy, fallback)
|
||||
self.assertLess(fallback, volatile_store)
|
||||
self.assertLess(volatile_store, store_begin)
|
||||
self.assertIn("beginInternalPrimaryFilesystemSafely", setup)
|
||||
self.assertGreaterEqual(primary_helper.count("delay(3000);"), 2)
|
||||
self.assertIn("fs.format()", primary_helper)
|
||||
self.assertIn("ReinitializedUnreadable", primary_helper)
|
||||
# Never call the core override in the nRF path: it auto-erases a
|
||||
# nonblank filesystem after any mount failure. The sole occurrence is
|
||||
# the separately scoped STM32 fallback.
|
||||
self.assertEqual(setup.count("InternalFS.begin();"), 1)
|
||||
self.assertGreater(setup.index("#else\n InternalFS.begin();", primary_policy), primary_format)
|
||||
self.assertGreater(setup.index("#else\n InternalFS.begin();", primary_policy), fallback)
|
||||
self.assertIn("store.disableSecondaryFS(true);", setup)
|
||||
secondary_geometry = setup.index(
|
||||
"const bool extra_fs_geometry_valid ="
|
||||
|
||||
@@ -69,16 +69,28 @@ int main() {
|
||||
store = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
|
||||
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
device = (ROOT / "examples/companion_radio/ResilientInternalExtraFS.cpp").read_text()
|
||||
device_header = (
|
||||
ROOT / "examples/companion_radio/ResilientInternalExtraFS.h"
|
||||
).read_text()
|
||||
self.assertIn("reinitializeInternalExtraFS(true)", store)
|
||||
self.assertIn("scan_physical_pages && !extra->scanAndRetireBadPages()", store)
|
||||
self.assertIn("extra->requestBootScan()", store)
|
||||
self.assertIn("extra->requestBootScan(true)", store)
|
||||
self.assertIn("setBootScanRequest(marker)", device)
|
||||
self.assertIn("BOOT_SCAN_ALL", device)
|
||||
self.assertIn("BOOT_SCAN_BAD_PAGE_BASE = 0x20", device_header)
|
||||
self.assertIn("BOOT_SCAN_REPEAT = 0x3E", device_header)
|
||||
self.assertIn("BOOT_SCAN_ALL = 0x3F", device_header)
|
||||
self.assertNotIn("BOOT_SCAN_REPEAT_BASE", device_header)
|
||||
page_markers = set(range(0x20, 0x20 + 25))
|
||||
recovery_markers = page_markers | {0x3E, 0x3F}
|
||||
ota_markers = {0x51, 0x53, 0xA6, 0xD4, 0xED}
|
||||
ota_markers.update(range(0x90, 0xA0))
|
||||
ota_markers.update(range(0xB1, 0xC0))
|
||||
ota_markers.update(range(0xC1, 0xCA))
|
||||
self.assertFalse(recovery_markers & ota_markers)
|
||||
self.assertIn("uint32_t bad = _bad_pages;", device)
|
||||
self.assertNotIn("_bad_pages | (_pending_pages & PAGE_MASK)", device)
|
||||
self.assertIn("return _boot_scan_forced ||", (
|
||||
ROOT / "examples/companion_radio/ResilientInternalExtraFS.h"
|
||||
).read_text())
|
||||
self.assertIn("return _boot_scan_forced ||", device_header)
|
||||
self.assertIn("extra->acknowledgeRecoveredBootHint()", store)
|
||||
self.assertIn("extra->pageMapNeedsSave()", store)
|
||||
scan_command = mesh.split("void MyMesh::scanInternalExtraFS(Stream& output)", 1)[1]
|
||||
|
||||
@@ -14,13 +14,32 @@ spec.loader.exec_module(module)
|
||||
|
||||
class Nrf52InternalFlashFixTest(unittest.TestCase):
|
||||
def test_wait_cannot_be_optimized_into_immediate_success(self):
|
||||
source = "before\n" + module.OLD_WAIT + " do_wait();\n }\nafter\n"
|
||||
source = ("before\n" + module.OLD_WAIT + " do_wait();\n }\n"
|
||||
+ module.OLD_FLUSH + module.OLD_VERIFY + "after\n")
|
||||
patched = module.patched_source(source)
|
||||
self.assertIn("volatile uint8_t sd_en", patched)
|
||||
self.assertIn('''__asm volatile ("" ::: "memory");''', patched)
|
||||
self.assertIn("if (sd_en)", patched)
|
||||
self.assertIn("mesh_flash_nrf5x_flush_checked", patched)
|
||||
self.assertIn("volatile uint8_t const * flash", patched)
|
||||
self.assertEqual(module.patched_source(patched), patched)
|
||||
|
||||
def test_cache_failures_are_sticky_until_checked_sync(self):
|
||||
patched = module.patched_cache_source(module.OLD_CACHE_FLUSH)
|
||||
self.assertIn("mesh_flash_cache_take_flush_result", patched)
|
||||
self.assertIn("mesh_flash_cache_flush_ok = false", patched)
|
||||
self.assertIn("fc->program", patched)
|
||||
self.assertIn("fc->verify", patched)
|
||||
self.assertEqual(module.patched_cache_source(patched), patched)
|
||||
|
||||
def test_internalfs_propagates_checked_flush_failure(self):
|
||||
source = (module.OLD_INTERNAL_READ + module.OLD_INTERNAL_PROG
|
||||
+ module.OLD_INTERNAL_ERASE_WRITE + module.OLD_INTERNAL_SYNC)
|
||||
patched = module.patched_internal_fs_source(source)
|
||||
self.assertIn("== (int) size", patched)
|
||||
self.assertIn("mesh_flash_nrf5x_flush_checked() ? 0 : LFS_ERR_IO", patched)
|
||||
self.assertEqual(module.patched_internal_fs_source(patched), patched)
|
||||
|
||||
def test_unrecognized_framework_fails_closed(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
module.patched_source("changed SDK layout")
|
||||
@@ -29,6 +48,8 @@ class Nrf52InternalFlashFixTest(unittest.TestCase):
|
||||
ini = (ROOT / "platformio.ini").read_text(encoding="utf-8")
|
||||
self.assertIn("pre:scripts/nrf52_internal_flash_fix.py", ini)
|
||||
self.assertIn('"*flash_nrf5x.c"', SCRIPT.read_text())
|
||||
self.assertIn('"*flash_cache.c"', SCRIPT.read_text())
|
||||
self.assertIn('"*InternalFileSystem.cpp"', SCRIPT.read_text())
|
||||
# The hardware-in-loop images intentionally bypass nrf52_base, but
|
||||
# still use the same framework and must not regress to its unsafe SVC
|
||||
# flash-completion wait.
|
||||
|
||||
Reference in new issue
Block a user