Fix nRF52 flash completion and preserve recoverable ExtraFS

This commit is contained in:
mikecarper committed 2026-09-22 09:29:05 -07:00
1 parent a9d7692bf3
commit 3ce499a700
20 files changed
+970 -33

No files matched your search

+71 -6
View File
@@ -210,7 +210,7 @@ void DataStore::begin() {
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
#elif defined(EXTRAFS)
#include <CustomLFS.h>
#include "ResilientInternalExtraFS.h"
#else
#include <InternalFileSystem.h>
#endif
@@ -280,8 +280,10 @@ static void cleanupAtomicTempFiles(FILESYSTEM* fs) {
bool DataStore::recoverInternalExtraFSOnBoot() {
if (_configuredFsExtra == nullptr) return false;
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
ResilientInternalExtraFS* extra =
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
if (_primary_storage_unavailable
|| !extra->pageMapReady()
|| !mesh::storage::isExpectedInternalExtraFsGeometry(
extra->getFlashAddr(), extra->getFlashSize(), extra->getBlockSize())
|| !mesh::storage::isInternalExtraFsReservedByApplication(
@@ -291,6 +293,19 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
return false;
}
if (extra->recoveryPending()) {
MESH_DEBUG_PRINTLN("DataStore: running requested boot-time ExtraFS scan");
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;
}
const mesh::storage::InternalSecondaryFsRecoveryResult result =
mesh::storage::recoverInternalSecondaryFilesystem(
[this]() -> bool {
@@ -306,14 +321,16 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
return true;
},
[this]() -> bool {
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS remains unusable; rebuilding 100 KiB (secondary-only data may be lost)");
return reinitializeInternalExtraFS();
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS remains unusable after mount retry; scanning and rebuilding (secondary-only data may be lost)");
return reinitializeInternalExtraFS(true);
});
if (result == mesh::storage::InternalSecondaryFsRecoveryResult::Failed) {
disableSecondaryFS(true);
return false;
}
extra->acknowledgeRecoveredBootHint();
// This runs only before migration and user-data loading. Clear the initial
// mount quarantine, not errors from a later incomplete contact/prefs load.
// Migration below establishes authority again and can re-latch any error.
@@ -324,13 +341,15 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
return true;
}
bool DataStore::reinitializeInternalExtraFS() {
bool DataStore::reinitializeInternalExtraFS(bool scan_physical_pages) {
if (_configuredFsExtra == nullptr) return false;
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
ResilientInternalExtraFS* extra =
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
if (!mesh::storage::isExpectedInternalExtraFsGeometry(
extra->getFlashAddr(), extra->getFlashSize(),
extra->getBlockSize())
|| !extra->pageMapReady()
|| !mesh::storage::isInternalExtraFsReservedByApplication(
(uint32_t)(uintptr_t)__flash_arduino_end)) {
_fsExtra = nullptr;
@@ -338,6 +357,16 @@ bool DataStore::reinitializeInternalExtraFS() {
return false;
}
// A physical test is destructive and must run only during early boot, before
// radio/UI tasks can issue competing SoftDevice flash operations. Ordinary
// USB repair and factory-reset paths retain the already-verified page map.
if ((scan_physical_pages && !extra->scanAndRetireBadPages())
|| (extra->pageMapNeedsSave() && !extra->savePageMap(*_fs))) {
_fsExtra = nullptr;
MESH_DEBUG_PRINTLN("DataStore: ExtraFS physical scan or bad-page map save failed");
return false;
}
const mesh::storage::InternalSecondaryFsRepairResult result =
mesh::storage::repairInternalSecondaryFilesystem(
[extra]() -> bool {
@@ -359,12 +388,15 @@ bool DataStore::reinitializeInternalExtraFS() {
_fsExtra = nullptr;
switch (result) {
case mesh::storage::InternalSecondaryFsRepairResult::FormatFailed:
extra->setStage(ResilientInternalExtraFS::Stage::FormatFailed);
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair format failed");
break;
case mesh::storage::InternalSecondaryFsRepairResult::MountFailed:
extra->setStage(ResilientInternalExtraFS::Stage::MountFailed);
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair mount failed");
break;
case mesh::storage::InternalSecondaryFsRepairResult::ValidationFailed:
extra->setStage(ResilientInternalExtraFS::Stage::ValidationFailed);
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair validation failed");
break;
default:
@@ -374,6 +406,7 @@ bool DataStore::reinitializeInternalExtraFS() {
}
_fsExtra = _configuredFsExtra;
extra->setStage(ResilientInternalExtraFS::Stage::Repaired);
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repaired and reactivated");
return true;
}
@@ -648,6 +681,38 @@ bool DataStore::repairInternalExtraFS() {
#endif
}
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
bool DataStore::requestInternalExtraFSBootScan() {
if (_primary_storage_unavailable || _configuredFsExtra == nullptr) {
return false;
}
ResilientInternalExtraFS* extra =
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
return extra->requestBootScan();
}
bool DataStore::formatInternalExtraFSHealth(char* reply,
size_t reply_size) const {
if (reply == nullptr || reply_size == 0 || _configuredFsExtra == nullptr) {
return false;
}
const ResilientInternalExtraFS* extra =
static_cast<const ResilientInternalExtraFS*>(_configuredFsExtra);
snprintf(reply, reply_size,
"ExtraFS %s; bad=0x%07lX; detected=0x%07lX; pending=0x%07lX; usable=%luK; mount=%s; last=%s; scanned=%u; boot=0x%02X",
extra->pageMapReady() ? "mapped" : "map-unreadable",
(unsigned long)extra->badPages(),
(unsigned long)extra->detectedBadPages(),
(unsigned long)extra->pendingPages(),
(unsigned long)(extra->usableBytes() / 1024),
_fsExtra == _configuredFsExtra ? "active" : "unavailable",
ResilientInternalExtraFS::stageName(extra->stage()),
(unsigned)extra->scannedPages(),
(unsigned)extra->bootMarkerAtInit());
return true;
}
#endif
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
if (!identity_store.recover("_main")) {
+5 -1
View File
@@ -90,7 +90,7 @@ class DataStore
void resetContactPageState(bool clear_incomplete = false);
#if defined(EXTRAFS) && !defined(QSPIFLASH)
bool recoverInternalExtraFSOnBoot();
bool reinitializeInternalExtraFS();
bool reinitializeInternalExtraFS(bool scan_physical_pages = false);
#endif
#endif
@@ -106,6 +106,10 @@ public:
void begin();
bool formatFileSystem();
bool repairInternalExtraFS();
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
bool requestInternalExtraFSBootScan();
bool formatInternalExtraFSHealth(char* reply, size_t reply_size) const;
#endif
FILESYSTEM* getPrimaryFS() const { return _fs; }
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
void markPrimaryFSUnavailable();
+35 -1
View File
@@ -2676,6 +2676,12 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
return true;
}
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
if (strcmp(command, "get flash.health") == 0) {
return _store->formatInternalExtraFSHealth(reply, reply_size);
}
#endif
if (strncmp(command, "get ", 4) == 0 &&
mesh::cli::handleStorageLayoutGet(command + 4, board, reply,
reply_size)) {
@@ -6036,7 +6042,9 @@ void MyMesh::repairInternalExtraFS(Stream& output) {
const bool contacts_saved = _store->saveContacts(this, save_filter);
const bool channels_saved = saveChannels();
const bool capacity_ready = _store->getStorageTotalKb() == 100;
const uint32_t capacity_kib = _store->getStorageTotalKb();
const bool capacity_ready = capacity_kib >= 84 && capacity_kib <= 100
&& (capacity_kib % 4) == 0;
if (contacts_saved && channels_saved && capacity_ready
&& !_store->hasPendingContactWrites()) {
mesh::resetLazyPersistenceAfterSuccess(
@@ -6054,6 +6062,24 @@ void MyMesh::repairInternalExtraFS(Stream& output) {
}
}
void MyMesh::scanInternalExtraFS(Stream& output) {
if (!_store->requestInternalExtraFSBootScan()) {
output.println(" Error: could not record the boot-time ExtraFS scan request");
char health[160];
if (_store->formatInternalExtraFSHealth(health, sizeof(health))) {
output.printf(" > %s\n", health);
}
return;
}
output.println(" > boot-time physical scan scheduled; ExtraFS files will be erased; rebooting");
// This command deliberately discards the secondary filesystem. The normal
// scheduled-reboot path refuses to reset when unsaved contacts exist, which
// would strand the durable scan request on a damaged store.
output.flush();
delay(1000);
board.reboot();
}
#if defined(MESHCORE_EXTRAFS_HIL)
bool MyMesh::handleExtraFsHilCommand(const char* command, char* reply,
size_t reply_size) {
@@ -8349,6 +8375,8 @@ void MyMesh::handleTerminalCommand(char* command) {
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
} else if (strcmp(command, "repair extrafs") == 0) {
repairInternalExtraFS(terminalOutput());
} else if (strcmp(command, "scan extrafs erase") == 0) {
scanInternalExtraFS(terminalOutput());
#endif
} else if (strcmp(command, "ver") == 0) {
terminalOutput().printf("Companion %s (protocol %u, build %s)\r\n",
@@ -8362,6 +8390,9 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().print(" board\r\n");
terminalOutput().print(" version\r\n");
terminalOutput().print(" get storage.layout\r\n");
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
terminalOutput().print(" get flash.health\r\n");
#endif
#if COMPANION_FEATURE_READER
terminalOutput().print(" get reader <chapter>:<verse> (World English Bible, offline)\r\n");
#endif
@@ -8453,6 +8484,7 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().print(" channel <name-or-slot> <text>\r\n");
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
terminalOutput().print(" repair extrafs (erases/rebuilds internal ExtraFS)\r\n");
terminalOutput().print(" scan extrafs erase (USB only; reboot for destructive page test)\r\n");
#endif
#if COMPANION_FEATURE_TEMP_RADIO || COMPANION_FEATURE_OTA_CLI
#if COMPANION_FEATURE_TEMP_RADIO
@@ -8899,6 +8931,8 @@ void MyMesh::checkCLIRescueCmd() {
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
} else if (strcmp(cli_command, "repair extrafs") == 0) {
repairInternalExtraFS(output);
} else if (strcmp(cli_command, "scan extrafs erase") == 0) {
scanInternalExtraFS(output);
#endif
} else if (strcmp(cli_command, "rebuild") == 0) {
bool success = _store->formatFileSystem();
+1
View File
@@ -512,6 +512,7 @@ private:
uint16_t released_slot);
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
void repairInternalExtraFS(Stream& output);
void scanInternalExtraFS(Stream& output);
#if defined(MESHCORE_EXTRAFS_HIL)
bool handleExtraFsHilCommand(const char* command, char* reply,
size_t reply_size);
@@ -0,0 +1,438 @@
#include "ResilientInternalExtraFS.h"
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
#include <helpers/IdentityStore.h>
#include <helpers/AtomicFileWriter.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
#if defined(NRF52840_XXAA)
constexpr uint32_t PRIMARY_START = 0xED000UL;
#else
constexpr uint32_t PRIMARY_START = 0x6D000UL;
#endif
constexpr uint32_t PRIMARY_SIZE = 7UL * FLASH_NRF52_PAGE_SIZE;
StaticSemaphore_t shared_flash_lock_storage;
SemaphoreHandle_t shared_flash_lock = nullptr;
void feedInheritedWatchdog() {
if (NRF_WDT->RUNSTATUS == 0) return;
const uint32_t enabled = NRF_WDT->RREN & 0xFFUL;
for (uint8_t channel = 0; channel < 8; ++channel) {
if ((enabled & (1UL << channel)) != 0) {
NRF_WDT->RR[channel] = WDT_RR_RR_Reload;
}
}
}
class FlashLock {
public:
FlashLock() {
if (shared_flash_lock == nullptr) {
shared_flash_lock = xSemaphoreCreateMutexStatic(&shared_flash_lock_storage);
}
xSemaphoreTake(shared_flash_lock, portMAX_DELAY);
}
~FlashLock() { xSemaphoreGive(shared_flash_lock); }
};
int primaryRead(const struct lfs_config*, lfs_block_t block, lfs_off_t off,
void* buffer, lfs_size_t size) {
FlashLock lock;
return flash_nrf5x_read(buffer, PRIMARY_START + block * 128 + off, size)
== static_cast<int>(size) ? 0 : LFS_ERR_IO;
}
int primaryProg(const struct lfs_config*, lfs_block_t block, lfs_off_t off,
const void* buffer, lfs_size_t size) {
FlashLock lock;
return flash_nrf5x_write(PRIMARY_START + block * 128 + off, buffer, size)
== static_cast<int>(size) ? 0 : LFS_ERR_IO;
}
int primaryErase(const struct lfs_config*, lfs_block_t block) {
FlashLock lock;
const uint32_t address = PRIMARY_START + block * 128;
for (uint32_t offset = 0; offset < 128; offset++) {
if (flash_nrf5x_write8(address + offset, 0xFF) != 1) return LFS_ERR_IO;
}
return 0;
}
int primarySync(const struct lfs_config*) {
FlashLock lock;
flash_nrf5x_flush();
return 0;
}
struct PageMapRecord {
uint32_t magic;
uint32_t bad_pages;
uint32_t pending_pages;
uint32_t check;
};
} // namespace
using mesh::storage::countInternalExtraFsBadPages;
struct lfs_config* ResilientInternalExtraFS::primaryConfig() {
static struct lfs_config config = {};
config.read = primaryRead;
config.prog = primaryProg;
config.erase = primaryErase;
config.sync = primarySync;
config.read_size = 128;
config.prog_size = 128;
config.block_size = 128;
config.block_count = PRIMARY_SIZE / 128;
config.lookahead = 128;
return &config;
}
ResilientInternalExtraFS::ResilientInternalExtraFS(
uint32_t flash_addr, uint32_t flash_size, uint32_t block_size)
: CustomLFS(flash_addr, flash_size, block_size) {
// Capture this before board power management can consume GPREGRET2. The
// 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) {
_boot_scan_requested = true;
_boot_scan_forced = true;
} else if (retained >= BOOT_SCAN_BAD_PAGE_BASE
&& retained < BOOT_SCAN_BAD_PAGE_BASE + PAGE_COUNT) {
_retained_bad_page = retained - BOOT_SCAN_BAD_PAGE_BASE;
_boot_scan_requested = true;
}
_configure_lfs();
}
void ResilientInternalExtraFS::_configure_lfs() {
CustomLFS::_configure_lfs();
_lfs_config.read = readCallback;
_lfs_config.prog = progCallback;
_lfs_config.erase = eraseCallback;
_lfs_config.sync = syncCallback;
_lfs_config.block_count =
(PAGE_COUNT - countInternalExtraFsBadPages(_bad_pages)) * BLOCKS_PER_PAGE;
}
uint32_t ResilientInternalExtraFS::usableBytes() const {
return (PAGE_COUNT - countInternalExtraFsBadPages(_bad_pages)) * PAGE_SIZE;
}
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) {
_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) {
return false;
}
_bad_pages = record.bad_pages;
_pending_pages = record.pending_pages;
_recorded_bad_pages = record.bad_pages;
_recorded_pending_pages = record.pending_pages;
_configure_lfs();
_map_ready = true;
return true;
}
bool ResilientInternalExtraFS::savePageMap(Adafruit_LittleFS& primary) {
if (!_map_ready) return false;
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();
if (saved) {
_recorded_bad_pages = _bad_pages;
_recorded_pending_pages = _pending_pages;
}
_stage = saved ? Stage::MapSaved : Stage::MapSaveFailed;
return saved;
}
bool ResilientInternalExtraFS::pageMapNeedsSave() const {
return _bad_pages != _recorded_bad_pages
|| _pending_pages != _recorded_pending_pages;
}
bool ResilientInternalExtraFS::setBootScanRequest(uint8_t value) {
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
if (sd_enabled) {
volatile uint32_t readback = 0;
return sd_power_gpregret_clr(1, 0xFF) == NRF_SUCCESS
&& sd_power_gpregret_set(1, value) == NRF_SUCCESS
&& sd_power_gpregret_get(1, (uint32_t*)&readback) == NRF_SUCCESS
&& static_cast<uint8_t>(readback) == value;
}
NRF_POWER->GPREGRET2 = value;
return static_cast<uint8_t>(NRF_POWER->GPREGRET2) == value;
}
bool ResilientInternalExtraFS::clearBootScanRequest() {
if (!_boot_scan_requested) return true;
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
if (sd_enabled) {
if (sd_power_gpregret_clr(1, 0xFF) != NRF_SUCCESS) return false;
} else {
NRF_POWER->GPREGRET2 = 0;
}
_boot_scan_requested = false;
return true;
}
bool ResilientInternalExtraFS::requestBootScan() {
if (!_map_ready) 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) {
for (uint8_t page = 0; page < PAGE_COUNT; ++page) {
if ((_pending_pages & (1UL << page)) != 0) {
marker = BOOT_SCAN_BAD_PAGE_BASE + page;
break;
}
}
}
return setBootScanRequest(marker);
}
void ResilientInternalExtraFS::acknowledgeRecoveredBootHint() {
// A healthy mount and successful file traversal are enough to preserve
// 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;
}
}
const char* ResilientInternalExtraFS::stageName(Stage stage) {
switch (stage) {
case Stage::Ready: return "ready";
case Stage::Scanning: return "scanning";
case Stage::TooManyBadPages: return "too-many-bad-pages";
case Stage::Scanned: return "scanned";
case Stage::MapSaveFailed: return "map-save-failed";
case Stage::MapSaved: return "map-saved";
case Stage::FormatFailed: return "format-failed";
case Stage::MountFailed: return "mount-failed";
case Stage::ValidationFailed: return "validation-failed";
case Stage::Repaired: return "repaired";
}
return "unknown";
}
uint32_t ResilientInternalExtraFS::physicalPageForBlock(
lfs_block_t block) const {
if (block >= _lfs_config.block_count) return INVALID_PAGE;
return mesh::storage::internalExtraFsPhysicalPage(
_bad_pages, block / BLOCKS_PER_PAGE);
}
void ResilientInternalExtraFS::retirePending(uint32_t physical_page) {
if (physical_page < PAGE_COUNT) {
_pending_pages |= 1UL << physical_page;
}
_io_failed = true;
_dirty = false;
_cached_page = INVALID_PAGE;
}
int ResilientInternalExtraFS::commitPage() {
if (_io_failed) return LFS_ERR_CORRUPT;
if (!_dirty) return 0;
const uint32_t address = _flash_addr + _cached_page * PAGE_SIZE;
if (flash_nrf5x_write(address, _page_buffer, PAGE_SIZE) != PAGE_SIZE) {
retirePending(_cached_page);
return LFS_ERR_CORRUPT;
}
flash_nrf5x_flush();
// 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) {
retirePending(_cached_page);
return LFS_ERR_CORRUPT;
}
_dirty = false;
return 0;
}
int ResilientInternalExtraFS::loadPage(uint32_t physical_page) {
if (_io_failed || physical_page >= PAGE_COUNT) return LFS_ERR_CORRUPT;
if (physical_page == _cached_page) return 0;
const int result = commitPage();
if (result != 0) return result;
flash_nrf5x_flush();
memcpy(_page_buffer,
reinterpret_cast<const void*>(_flash_addr + physical_page * PAGE_SIZE),
PAGE_SIZE);
_cached_page = physical_page;
return 0;
}
int ResilientInternalExtraFS::readCallback(
const struct lfs_config* config, lfs_block_t block, lfs_off_t off,
void* buffer, lfs_size_t size) {
FlashLock lock;
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
const uint32_t page = fs->physicalPageForBlock(block);
if (page == INVALID_PAGE || off > fs->_block_size
|| size > fs->_block_size - off || fs->loadPage(page) != 0) {
return LFS_ERR_CORRUPT;
}
memcpy(buffer, fs->_page_buffer + (block % BLOCKS_PER_PAGE) * fs->_block_size
+ off, size);
return 0;
}
int ResilientInternalExtraFS::progCallback(
const struct lfs_config* config, lfs_block_t block, lfs_off_t off,
const void* buffer, lfs_size_t size) {
FlashLock lock;
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
const uint32_t page = fs->physicalPageForBlock(block);
if (page == INVALID_PAGE || off > fs->_block_size
|| size > fs->_block_size - off || fs->loadPage(page) != 0) {
return LFS_ERR_CORRUPT;
}
memcpy(fs->_page_buffer + (block % BLOCKS_PER_PAGE) * fs->_block_size + off,
buffer, size);
fs->_dirty = true;
return 0;
}
int ResilientInternalExtraFS::eraseCallback(
const struct lfs_config* config, lfs_block_t block) {
FlashLock lock;
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
const uint32_t page = fs->physicalPageForBlock(block);
if (page == INVALID_PAGE || fs->loadPage(page) != 0) {
return LFS_ERR_CORRUPT;
}
memset(fs->_page_buffer + (block % BLOCKS_PER_PAGE) * fs->_block_size,
0xFF, fs->_block_size);
fs->_dirty = true;
return 0;
}
int ResilientInternalExtraFS::syncCallback(const struct lfs_config* config) {
FlashLock lock;
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
return fs->commitPage();
}
bool ResilientInternalExtraFS::rawPageEquals(uint32_t address,
uint8_t value) const {
const volatile uint8_t* raw =
reinterpret_cast<const volatile uint8_t*>(address);
for (uint32_t offset = 0; offset < PAGE_SIZE; offset++) {
if (raw[offset] != value) return false;
}
return true;
}
bool ResilientInternalExtraFS::testPhysicalPage(uint32_t physical_page) {
const uint32_t address = _flash_addr + physical_page * PAGE_SIZE;
for (uint8_t attempt = 0; attempt < 2; attempt++) {
feedInheritedWatchdog();
flash_nrf5x_flush();
if (!flash_nrf5x_erase(address) || !rawPageEquals(address, 0xFF)) continue;
feedInheritedWatchdog();
memset(_page_buffer, 0, PAGE_SIZE);
if (flash_nrf5x_write(address, _page_buffer, PAGE_SIZE) != PAGE_SIZE) {
continue;
}
flash_nrf5x_flush();
if (!rawPageEquals(address, 0x00)) continue;
feedInheritedWatchdog();
if (flash_nrf5x_erase(address) && rawPageEquals(address, 0xFF)) {
return true;
}
}
return false;
}
bool ResilientInternalExtraFS::scanAndRetireBadPages() {
if (!_map_ready || _flash_addr != 0xD4000UL || _flash_total_size != 0x19000UL
|| _block_size != 128) return false;
if (!clearBootScanRequest()) return false;
end();
_cached_page = INVALID_PAGE;
_dirty = false;
_io_failed = false;
_stage = Stage::Scanning;
_scanned_pages = 0;
_detected_bad_pages = 0;
// A runtime readback mismatch is a reason to scan, not proof of a dead
// physical page: SoftDevice contention can produce transient failures.
// Retire only a page that fails the isolated early-boot erase/write/read
// test on both attempts.
uint32_t bad = _bad_pages;
if (countInternalExtraFsBadPages(bad) > MAX_BAD_PAGES) {
_stage = Stage::TooManyBadPages;
return false;
}
for (uint32_t page = 0; page < PAGE_COUNT; page++) {
feedInheritedWatchdog();
if ((bad & (1UL << page)) != 0) continue;
if (!testPhysicalPage(page)) {
bad |= 1UL << page;
_detected_bad_pages |= 1UL << page;
if (countInternalExtraFsBadPages(bad) > MAX_BAD_PAGES) {
_stage = Stage::TooManyBadPages;
return false;
}
}
_scanned_pages++;
}
_bad_pages = bad;
_pending_pages = 0;
_configure_lfs();
_stage = Stage::Scanned;
return true;
}
#endif
@@ -0,0 +1,101 @@
#pragma once
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
#include <CustomLFS.h>
#include <helpers/nrf52/InternalExtraFsPageMap.h>
// The T1000-E and other nRF52 internal-ExtraFS companions use 25 physical
// 4 KiB pages. LittleFS sees 128-byte blocks; a failed physical page must be
// removed as a whole, not treated as 32 unrelated bad blocks.
class ResilientInternalExtraFS : public CustomLFS {
public:
enum class Stage : uint8_t {
Ready, Scanning, TooManyBadPages, Scanned, MapSaveFailed,
MapSaved, FormatFailed, MountFailed, ValidationFailed, Repaired
};
static constexpr uint32_t PAGE_COUNT =
mesh::storage::INTERNAL_EXTRAFS_PAGE_COUNT;
static constexpr uint32_t PAGE_SIZE = FLASH_NRF52_PAGE_SIZE;
static constexpr uint32_t BLOCKS_PER_PAGE = PAGE_SIZE / 128;
ResilientInternalExtraFS(uint32_t flash_addr, uint32_t flash_size,
uint32_t block_size);
static struct lfs_config* primaryConfig();
// Read the per-device page map from primary identity storage before the
// first secondary mount. An unreadable map must never be guessed as empty.
bool loadPageMap(Adafruit_LittleFS& primary);
bool pageMapReady() const { return _map_ready; }
bool recoveryPending() const {
return _boot_scan_forced || (_pending_pages & SCAN_REQUEST) != 0;
}
bool ioFailed() const { return _io_failed; }
uint32_t badPages() const { return _bad_pages; }
uint32_t pendingPages() const { return _pending_pages; }
uint32_t usableBytes() const;
Stage stage() const { return _stage; }
uint8_t scannedPages() const { return _scanned_pages; }
uint32_t detectedBadPages() const { return _detected_bad_pages; }
uint8_t bootMarkerAtInit() const { return _boot_marker_at_init; }
static const char* stageName(Stage stage);
void setStage(Stage stage) { _stage = stage; }
// Only call after ExtraFS has been unmounted and its contents have been
// declared unrecoverable. Tests every physical page by erase / program /
// direct flash readback / erase, then maps LittleFS around bad pages.
bool scanAndRetireBadPages();
bool savePageMap(Adafruit_LittleFS& primary);
bool pageMapNeedsSave() const;
bool requestBootScan();
void acknowledgeRecoveredBootHint();
protected:
void _configure_lfs() override;
private:
static constexpr uint32_t INVALID_PAGE = 0xFFFFFFFFUL;
static constexpr uint32_t PAGE_MASK =
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;
uint32_t _bad_pages = 0;
uint32_t _pending_pages = 0;
bool _boot_scan_requested = false;
bool _boot_scan_forced = false;
uint8_t _boot_marker_at_init = 0;
uint8_t _retained_bad_page = 0xFF;
uint32_t _recorded_bad_pages = 0;
uint32_t _recorded_pending_pages = 0;
uint32_t _cached_page = INVALID_PAGE;
bool _dirty = false;
bool _io_failed = false;
bool _map_ready = false;
Stage _stage = Stage::Ready;
uint8_t _scanned_pages = 0;
uint32_t _detected_bad_pages = 0;
alignas(4) uint8_t _page_buffer[PAGE_SIZE];
uint32_t physicalPageForBlock(lfs_block_t block) const;
int loadPage(uint32_t physical_page);
int commitPage();
bool testPhysicalPage(uint32_t physical_page);
bool rawPageEquals(uint32_t address, uint8_t value) const;
void retirePending(uint32_t physical_page);
bool setBootScanRequest(uint8_t value);
bool clearBootScanRequest();
static int readCallback(const struct lfs_config* config, lfs_block_t block,
lfs_off_t off, void* buffer, lfs_size_t size);
static int progCallback(const struct lfs_config* config, lfs_block_t block,
lfs_off_t off, const void* buffer, lfs_size_t size);
static int eraseCallback(const struct lfs_config* config, lfs_block_t block);
static int syncCallback(const struct lfs_config* config);
};
#endif
+33 -4
View File
@@ -198,9 +198,9 @@ static bool isNetworkTerminalActive();
DataStore store(InternalFS, QSPIFlash, rtc_clock);
#else
#if defined(EXTRAFS)
#include <CustomLFS.h>
#include "ResilientInternalExtraFS.h"
extern "C" uint32_t __flash_arduino_end[];
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
ResilientInternalExtraFS ExtraFS(0xD4000, 0x19000, 128);
DataStore store(InternalFS, ExtraFS, rtc_clock);
#else
DataStore store(InternalFS, rtc_clock);
@@ -2799,7 +2799,12 @@ void setup() {
const mesh::storage::InternalSecondaryFsBootResult primary_fs_boot =
mesh::storage::prepareInternalSecondaryFilesystem(
[]() -> bool {
#if defined(EXTRAFS) && !defined(QSPIFLASH)
return InternalFS.Adafruit_LittleFS::begin(
ResilientInternalExtraFS::primaryConfig());
#else
return InternalFS.Adafruit_LittleFS::begin();
#endif
},
[]() -> bool {
return mesh::storage::isErasedFlashRange(
@@ -2848,6 +2853,10 @@ 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)
&& ExtraFS.loadPageMap(InternalFS);
const bool extra_fs_geometry_valid =
mesh::storage::isExpectedInternalExtraFsGeometry(
ExtraFS.getFlashAddr(), ExtraFS.getFlashSize(),
@@ -2856,10 +2865,15 @@ void setup() {
(uint32_t)(uintptr_t)__flash_arduino_end);
// Initial mount is always non-destructive, even for blank media.
// DataStore validates primary first, then retries/rebuilds ExtraFS.
if (!extra_fs_geometry_valid
if (!extra_fs_map_ready || !extra_fs_geometry_valid
|| ExtraFS.recoveryPending()
|| !ExtraFS.Adafruit_LittleFS::begin()) {
if (!extra_fs_geometry_valid) {
if (!extra_fs_map_ready) {
MESH_DEBUG_PRINTLN("CustomLFS: bad-page map unreadable; preserving ExtraFS and blocking writes");
} else if (!extra_fs_geometry_valid) {
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS geometry is not 100 KiB; refusing to mount or erase it");
} else if (ExtraFS.recoveryPending()) {
MESH_DEBUG_PRINTLN("CustomLFS: recorded physical page fault; deferring scan until primary validation");
} else {
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS mount failed; deferring automatic recovery until primary validation");
}
@@ -3185,6 +3199,21 @@ void loop() {
expireUsbBinaryStartupProbeBeforeDispatch();
#endif
the_mesh.loop();
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
// The flash-page number is retained across a software reset without trying
// to write primary identity storage while radio/BLE tasks are running.
// Early boot then verifies and records the page map before remounting.
if (ExtraFS.ioFailed()) {
static uint32_t last_fault_record_attempt = 0;
if (static_cast<uint32_t>(millis() - last_fault_record_attempt) >= 5000) {
last_fault_record_attempt = millis();
if (ExtraFS.requestBootScan()) {
MESH_DEBUG_PRINTLN("ExtraFS: physical write failed; rebooting for isolated page scan");
board.reboot();
}
}
}
#endif
#ifdef RECOVERABLE_EXTERNAL_RADIO
serviceCompanionRadioRecovery();
#endif
+1
View File
@@ -154,6 +154,7 @@ platform_packages =
platformio/toolchain-gccarmnoneeabi@^1.140201.0
extra_scripts = create-uf2.py
pre:scripts/nrf52_usb_power_fix.py
pre:scripts/nrf52_internal_flash_fix.py
pre:scripts/nrf52_internal_bootloader_link.py
; Nordic's builder otherwise overwrites the LTO-aware archive tools.
pre:scripts/nrf52_lto_toolchain.py
+59
View File
@@ -0,0 +1,59 @@
"""Build-local fix for the nRF52 InternalFS SoftDevice flash wait.
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.
"""
from pathlib import Path
OLD_WAIT = """ uint8_t sd_en = 0;
(void) sd_softdevice_is_enabled(&sd_en);
if (sd_en) {
"""
FIXED_WAIT = """ volatile uint8_t sd_en = 0;
(void) sd_softdevice_is_enabled((uint8_t*) &sd_en);
__asm volatile ("" ::: "memory");
if (sd_en) {
"""
def patched_source(source):
source = source.replace("\r\n", "\n")
if FIXED_WAIT in source and OLD_WAIT not in source:
return source
if source.count(OLD_WAIT) != 1:
raise RuntimeError(
"nRF52 flash fix: unrecognized InternalFS flash driver; review "
"the framework update before building (shared SDK not modified)"
)
return source.replace(OLD_WAIT, FIXED_WAIT)
def replace_driver(build_env, node):
source = Path(node.srcnode().get_abspath())
patched = patched_source(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")
return build_env.File(str(destination))
def install(build_env):
build_env.AddBuildMiddleware(
replace_driver,
"*InternalFileSytem*src*flash*flash_nrf5x.c",
)
if "Import" in globals():
Import("env")
install(env)
+10 -9
View File
@@ -2,6 +2,7 @@
#include "NRF52Board.h"
#include "PowerManagementUtils.h"
#include "Nrf52BootloaderVersion.h"
#include "nrf52/SoftDeviceState.h"
#include <target.h>
#ifdef USER_GPIO_CONTROL
#include "UserGpioPinPolicy.h"
@@ -103,7 +104,7 @@ bool NRF52Board::isUserGpioAvailable(uint8_t pin) const {
bool NRF52Board::rebootToUf2Bootloader() {
uint8_t sd_enabled = 0;
if (sd_softdevice_is_enabled(&sd_enabled) != NRF_SUCCESS) return false;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
if (sd_enabled) {
if (sd_power_gpregret_clr(0, 0xFF) != NRF_SUCCESS
@@ -211,7 +212,7 @@ void NRF52Board::initPowerMgr() {
// Clear registers for next boot
// Note: At this point SoftDevice may or may not be enabled
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_reset_reason_clr(0xFFFFFFFF);
sd_power_gpregret_clr(1, 0xFF);
@@ -308,7 +309,7 @@ void NRF52Board::enterSystemOff(uint8_t reason) {
// Record shutdown reason in GPREGRET2
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_gpregret_clr(1, 0xFF);
sd_power_gpregret_set(1, reason);
@@ -399,7 +400,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
void NRF52Board::armVbusWake() {
// Configure VBUS (USB power) wake alongside (or instead of) LPCOMP.
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_usbdetected_enable(1);
} else {
@@ -415,7 +416,7 @@ void NRF52BoardDCDC::begin() {
// Enable DC/DC converter for improved power efficiency
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
} else {
@@ -426,7 +427,7 @@ void NRF52BoardDCDC::begin() {
bool NRF52Board::isExternalPowered() {
// Check if SoftDevice is enabled before using its API
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
uint32_t usb_status;
@@ -470,7 +471,7 @@ void NRF52Board::sleep(uint32_t secs) {
// On nRF52, we use event-driven sleep instead of timed sleep
// The 'secs' parameter is ignored - we wake on any interrupt
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
// A single call is required here. If an interrupt arrived since the last
@@ -492,7 +493,7 @@ void NRF52Board::sleep(uint32_t secs) {
// Temperature from NRF52 MCU
float NRF52Board::getMCUTemperature() {
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
uint32_t err_code;
int32_t temp;
@@ -587,7 +588,7 @@ void NRF52Board::powerOff() {
// Enter SYSTEMOFF
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) { // SoftDevice is enabled
sd_power_system_off();
} else { // SoftDevice is not enable
@@ -0,0 +1,41 @@
#pragma once
#include <stdint.h>
namespace mesh {
namespace storage {
static const uint32_t INTERNAL_EXTRAFS_PAGE_COUNT = 25;
static const uint32_t INTERNAL_EXTRAFS_PAGE_MASK =
(1UL << INTERNAL_EXTRAFS_PAGE_COUNT) - 1UL;
inline uint8_t countInternalExtraFsBadPages(uint32_t mask) {
uint8_t count = 0;
while (mask != 0) {
count += static_cast<uint8_t>(mask & 1U);
mask >>= 1;
}
return count;
}
// The old all-good layout is the identity map. Once a confirmed bad page is
// retired, every logical 4 KiB page after it moves to the next good physical
// page. A fixed 128-byte LittleFS block never straddles those page boundaries.
inline uint32_t internalExtraFsPhysicalPage(uint32_t bad_mask,
uint32_t logical_page) {
if ((bad_mask & ~INTERNAL_EXTRAFS_PAGE_MASK) != 0
|| logical_page >= INTERNAL_EXTRAFS_PAGE_COUNT
- countInternalExtraFsBadPages(bad_mask)) {
return UINT32_MAX;
}
for (uint32_t physical = 0; physical < INTERNAL_EXTRAFS_PAGE_COUNT;
physical++) {
if ((bad_mask & (1UL << physical)) != 0) continue;
if (logical_page == 0) return physical;
logical_page--;
}
return UINT32_MAX;
}
} // namespace storage
} // namespace mesh
+2 -1
View File
@@ -1,6 +1,7 @@
#include "SerialBLEInterface.h"
#include "../BluetoothMac.h"
#include "../CompanionFrameQueue.h"
#include "SoftDeviceState.h"
#include <stdio.h>
#include <string.h>
#include "ble_gap.h"
@@ -439,7 +440,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
if (!Bluefruit.begin()) {
uint8_t softdevice_enabled = 0;
const uint32_t softdevice_status =
sd_softdevice_is_enabled(&softdevice_enabled);
mesh_nrf52::softdeviceIsEnabled(softdevice_enabled);
ble_gap_addr_t address = {};
const uint32_t gap_status = softdevice_enabled
? sd_ble_gap_addr_get(&address) : NRF_ERROR_INVALID_STATE;
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <nrf_sdm.h>
namespace mesh_nrf52 {
// sd_softdevice_is_enabled is an SVC wrapper. With LTO, its assembly body
// does not tell GCC that the output pointer is written by the supervisor.
// Load the result from volatile storage after a compiler memory barrier.
inline uint32_t softdeviceIsEnabled(uint8_t& enabled) {
volatile uint8_t state = 0;
const uint32_t result = sd_softdevice_is_enabled((uint8_t*)&state);
__asm volatile ("" ::: "memory");
enabled = state;
return result;
}
} // namespace mesh_nrf52
+4 -3
View File
@@ -26,6 +26,7 @@
#include "nrf.h"
#include "nrf_soc.h"
#include "nrf_sdm.h"
#include "../nrf52/SoftDeviceState.h"
#if defined(OTA_SD_STORE)
#include "OtaStoreSdNrf52.h"
#endif
@@ -467,7 +468,7 @@ static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr;
static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
uint8_t sd_en = 0;
sd_softdevice_is_enabled(&sd_en);
mesh_nrf52::softdeviceIsEnabled(sd_en);
if (sd_en) { // POWER is SD-restricted while the SoftDevice runs
sd_power_gpregret_clr(1, 0xFFFFFFFF);
sd_power_gpregret_set(1, source);
@@ -481,7 +482,7 @@ static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
static bool ota_nrf52_clear_reset_reasons() {
uint8_t sd_en = 0;
if (sd_softdevice_is_enabled(&sd_en) != NRF_SUCCESS) return false;
if (mesh_nrf52::softdeviceIsEnabled(sd_en) != NRF_SUCCESS) return false;
if (sd_en) {
return sd_power_reset_reason_clr(0xFFFFFFFFu) == NRF_SUCCESS;
}
@@ -527,7 +528,7 @@ void ota_reboot_to_bootloader_update() {
source = GPREGRET2_OTA_STAGE_SD;
#endif
uint8_t sd_en = 0;
sd_softdevice_is_enabled(&sd_en);
mesh_nrf52::softdeviceIsEnabled(sd_en);
if (sd_en) {
sd_power_gpregret_clr(1, 0xFFFFFFFF);
sd_power_gpregret_set(1, source);
+5
View File
@@ -63,6 +63,11 @@ struct ble_gap_conn_params_t { unsigned min_conn_interval, max_conn_interval, sl
int sd_ble_gap_addr_set(ble_gap_addr_t*) { return NRF_SUCCESS; }
int sd_ble_gap_addr_get(ble_gap_addr_t* a) { memset(a, 0, sizeof(*a)); return NRF_SUCCESS; }
int sd_softdevice_is_enabled(uint8_t* enabled) { *enabled = 0; return NRF_SUCCESS; }
namespace mesh_nrf52 {
int softdeviceIsEnabled(uint8_t& enabled) {
return sd_softdevice_is_enabled(&enabled);
}
}
int sd_ble_gap_ppcp_set(ble_gap_conn_params_t*) { return NRF_SUCCESS; }
namespace mesh {
struct Logging { void println(const char*) {} };
+6 -6
View File
@@ -363,7 +363,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
main = MAIN.read_text(encoding="utf-8")
geometry = re.search(
r"CustomLFS\s+ExtraFS\s*\(\s*(0x[0-9A-Fa-f]+)\s*,\s*"
r"ResilientInternalExtraFS\s+ExtraFS\s*\(\s*(0x[0-9A-Fa-f]+)\s*,\s*"
r"(0x[0-9A-Fa-f]+)\s*,\s*128\s*\)",
main,
)
@@ -463,7 +463,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
ready_at = recover.index("validateLfsFilesystem(_fsExtra)", policy_at)
unmount_at = recover.index("extra->end()", ready_at)
remount_at = recover.index("extra->Adafruit_LittleFS::begin()", unmount_at)
repair_at = recover.index("reinitializeInternalExtraFS()", remount_at)
repair_at = recover.index("reinitializeInternalExtraFS(true)", remount_at)
self.assertLess(ready_at, unmount_at)
self.assertLess(unmount_at, remount_at)
self.assertLess(remount_at, repair_at)
@@ -475,7 +475,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
store = STORE.read_text(encoding="utf-8")
recover = function_body(store, "bool DataStore::recoverInternalExtraFSOnBoot()")
failure_at = recover.index("InternalSecondaryFsRecoveryResult::Failed")
clear_at = recover.index("_secondary_authority_unknown = false;")
clear_at = recover.rindex("_secondary_authority_unknown = false;")
failure = recover[failure_at:clear_at]
self.assertIn("disableSecondaryFS(true);", failure)
self.assertIn("return false;", failure)
@@ -509,7 +509,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
self.assertNotIn("format()", active_retry)
reinitialize = function_body(
store, "bool DataStore::reinitializeInternalExtraFS()"
store, "bool DataStore::reinitializeInternalExtraFS(bool scan_physical_pages)"
)
geometry_at = reinitialize.index("isExpectedInternalExtraFsGeometry(")
self.assertIn("isInternalExtraFsReservedByApplication(", reinitialize)
@@ -944,7 +944,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
# 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", primary_policy), primary_format)
self.assertGreater(setup.index("#else\n InternalFS.begin();", primary_policy), primary_format)
self.assertIn("store.disableSecondaryFS(true);", setup)
secondary_geometry = setup.index(
"const bool extra_fs_geometry_valid ="
@@ -980,7 +980,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
self.assertIn("_store->repairInternalExtraFS()", repair)
self.assertIn("_store->saveContacts(this, save_filter)", repair)
self.assertIn("const bool channels_saved = saveChannels();", repair)
self.assertIn("_store->getStorageTotalKb() == 100", repair)
self.assertIn("capacity_kib >= 84 && capacity_kib <= 100", repair)
self.assertIn("getStorageTotalKb()", repair)
rescue = function_body(mesh, "void MyMesh::checkCLIRescueCmd()")
+91
View File
@@ -0,0 +1,91 @@
"""Host coverage for physical-page retirement in nRF52 internal ExtraFS."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class InternalExtraFsPageMapTest(unittest.TestCase):
def test_mapping_skips_whole_bad_pages_and_keeps_legacy_identity_layout(self):
source = r"""
#include <helpers/nrf52/InternalExtraFsPageMap.h>
#include <stdint.h>
using mesh::storage::countInternalExtraFsBadPages;
using mesh::storage::internalExtraFsPhysicalPage;
int main() {
for (uint32_t p = 0; p < 25; ++p) {
if (internalExtraFsPhysicalPage(0, p) != p) return 1;
}
if (internalExtraFsPhysicalPage(0, 25) != UINT32_MAX) return 2;
const uint32_t middle = 1UL << 12;
if (countInternalExtraFsBadPages(middle) != 1) return 3;
if (internalExtraFsPhysicalPage(middle, 11) != 11) return 4;
if (internalExtraFsPhysicalPage(middle, 12) != 13) return 5;
if (internalExtraFsPhysicalPage(middle, 23) != 24) return 6;
if (internalExtraFsPhysicalPage(middle, 24) != UINT32_MAX) return 7;
const uint32_t edges = (1UL << 0) | (1UL << 24);
if (internalExtraFsPhysicalPage(edges, 0) != 1) return 8;
if (internalExtraFsPhysicalPage(edges, 22) != 23) return 9;
if (internalExtraFsPhysicalPage(edges, 23) != UINT32_MAX) return 10;
const uint32_t four = (1UL << 0) | (1UL << 4)
| (1UL << 17) | (1UL << 24);
if (countInternalExtraFsBadPages(four) != 4) return 11;
uint32_t previous = 0;
for (uint32_t logical = 0; logical < 21; logical++) {
const uint32_t physical = internalExtraFsPhysicalPage(four, logical);
if (physical >= 25 || (four & (1UL << physical))) return 12;
if (logical && physical <= previous) return 13;
previous = physical;
}
if (internalExtraFsPhysicalPage(four, 21) != UINT32_MAX) return 14;
if (internalExtraFsPhysicalPage(1UL << 25, 0) != UINT32_MAX) return 15;
return 0;
}
"""
with tempfile.TemporaryDirectory() as temp_dir:
executable = Path(temp_dir) / "extrafs_page_map"
compiled = subprocess.run(
["c++", "-std=c++11", f"-I{ROOT / 'src'}", "-x", "c++",
"-", "-o", str(executable)],
input=source,
text=True,
capture_output=True,
)
self.assertEqual(compiled.returncode, 0, compiled.stderr)
executed = subprocess.run([str(executable)], capture_output=True,
text=True)
self.assertEqual(executed.returncode, 0, executed.stderr)
def test_destructive_scan_only_runs_during_early_boot(self):
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()
self.assertIn("reinitializeInternalExtraFS(true)", store)
self.assertIn("scan_physical_pages && !extra->scanAndRetireBadPages()", store)
self.assertIn("extra->requestBootScan()", store)
self.assertIn("setBootScanRequest(marker)", device)
self.assertIn("BOOT_SCAN_ALL", device)
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("extra->acknowledgeRecoveredBootHint()", store)
self.assertIn("extra->pageMapNeedsSave()", store)
scan_command = mesh.split("void MyMesh::scanInternalExtraFS(Stream& output)", 1)[1]
scan_command = scan_command.split("#if defined(MESHCORE_EXTRAFS_HIL)", 1)[0]
self.assertIn("_store->requestInternalExtraFSBootScan()", scan_command)
self.assertNotIn("scanAndRetireBadPages", scan_command)
if __name__ == "__main__":
unittest.main()
+46
View File
@@ -0,0 +1,46 @@
"""Guard the build-local nRF52 SoftDevice flash-completion repair."""
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts/nrf52_internal_flash_fix.py"
spec = spec_from_file_location("nrf52_internal_flash_fix", SCRIPT)
module = module_from_spec(spec)
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"
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.assertEqual(module.patched_source(patched), patched)
def test_unrecognized_framework_fails_closed(self):
with self.assertRaises(RuntimeError):
module.patched_source("changed SDK layout")
def test_all_nrf52_builds_load_fix(self):
ini = (ROOT / "platformio.ini").read_text(encoding="utf-8")
self.assertIn("pre:scripts/nrf52_internal_flash_fix.py", ini)
self.assertIn("*InternalFileSytem*src*flash*flash_nrf5x.c", SCRIPT.read_text())
def test_application_softdevice_checks_use_compiler_safe_wrapper(self):
helper = ROOT / "src/helpers/nrf52/SoftDeviceState.h"
source = helper.read_text(encoding="utf-8")
self.assertIn("volatile uint8_t state", source)
self.assertIn('''__asm volatile ("" ::: "memory");''', source)
for path in (ROOT / "src", ROOT / "variants", ROOT / "examples/companion_radio"):
for source_file in path.rglob("*.cpp"):
self.assertNotIn("sd_softdevice_is_enabled(",
source_file.read_text(encoding="utf-8"),
str(source_file))
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -18,7 +18,7 @@ chat = (ROOT / "examples/simple_secure_chat/main.cpp").read_text()
assert "virtual bool rebootToUf2Bootloader() { return false; }" in main_board
assert "bool rebootToUf2Bootloader() override;" in nrf_board_h
assert "bool NRF52Board::rebootToUf2Bootloader()" in nrf_board_cpp
assert "sd_softdevice_is_enabled(&sd_enabled) != NRF_SUCCESS" in nrf_board_cpp
assert "mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS" in nrf_board_cpp
clear = nrf_board_cpp.index("sd_power_gpregret_clr(0, 0xFF)")
set_magic = nrf_board_cpp.index(
"sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET)"
+2 -1
View File
@@ -5,6 +5,7 @@
#include "nrf_soc.h"
#include "RAK4631Board.h"
#include "../../src/helpers/nrf52/SoftDeviceState.h"
#ifdef ETHERNET_ENABLED
// Drive WB_IO2 HIGH as early as possible using direct register access.
@@ -47,7 +48,7 @@ void RAK4631Board::begin() {
// merely skipping NRF52BoardDCDC::begin() would inherit any prior state.
NRF52Board::begin();
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_dcdc_mode_set(NRF_POWER_DCDC_DISABLE);
} else {