From a15e70502959cc278694201fe80dde11790488f3 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 13:44:49 +0800 Subject: [PATCH 1/8] stm32wl: add STM32WL to rmDir() recursive delete in FSCommon STM32_LittleFS already implements rmdir_r() which delegates to lfs_remove(). STM32WL was excluded from all branches in rmDir(), causing directory cleanup to silently do nothing. Extend the existing NRF52 branch to cover STM32WL. Signed-off-by: Andrew Yong --- src/FSCommon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index f215be80fb6..9c3b04a5f17 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -271,8 +271,8 @@ void rmDir(const char *dirname) #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) listDir(dirname, 10, true); -#elif defined(ARCH_NRF52) - // nRF52 implementation of LittleFS has a recursive delete function +#elif defined(ARCH_NRF52) || defined(ARCH_STM32WL) + // nRF52 and STM32WL implementations of LittleFS have a recursive delete function FSCom.rmdir_r(dirname); #endif From 57a49d946396f7d5d233b906578dd01ea1fa0750 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 13:50:37 +0800 Subject: [PATCH 2/8] stm32wl: add STM32WL to saveToDisk() format-on-retry LittleFS::begin() already formats on mount failure. Extend the existing NRF52 format-on-write-retry path to STM32WL so a failed save triggers a format + remount before the second attempt, matching the NRF52 recovery semantics. Signed-off-by: Andrew Yong --- src/mesh/NodeDB.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 4b08715660f..8a665c8614f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1604,7 +1604,8 @@ bool NodeDB::saveToDisk(int saveWhat) if (!success) { LOG_ERROR("Failed to save to disk, retrying"); -#ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion +#if defined(ARCH_NRF52) || \ + defined(ARCH_STM32WL) // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion spiLock->lock(); FSCom.format(); spiLock->unlock(); From 46c8983543fbe5354a0b61359cedb9acc5378e2a Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 13:51:34 +0800 Subject: [PATCH 3/8] stm32wl: use native lfs_rename() in renameFile() for STM32WL The non-ESP32 path in renameFile() does copyFile + remove, costing two full block write+erase cycles per rename. STM32_LittleFS::rename() delegates to lfs_rename() which is an atomic metadata-only operation with no extra flash wear. SafeFile uses renameFile() on every atomic close, so this directly reduces write amplification on the 28KB STM32WL filesystem. Signed-off-by: Andrew Yong --- src/FSCommon.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 9c3b04a5f17..c274959bbb6 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -80,10 +80,11 @@ bool renameFile(const char *pathFrom, const char *pathTo) { #ifdef FSCom -#ifdef ARCH_ESP32 +#if defined(ARCH_ESP32) || defined(ARCH_STM32WL) // take SPI Lock spiLock->lock(); - // rename was fixed for ESP32 IDF LittleFS in April + // ESP32: rename was fixed for IDF LittleFS in April + // STM32WL: STM32_LittleFS::rename() calls lfs_rename() which is atomic bool result = FSCom.rename(pathFrom, pathTo); spiLock->unlock(); return result; From 0acc819ca5c4e0bea1c266dd2b601e9db4735bff Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 13:52:22 +0800 Subject: [PATCH 4/8] stm32wl: check HAL_FLASH_Unlock() return in _internal_flash_erase _internal_flash_prog already checks HAL_FLASH_Unlock() and returns LFS_ERR_IO on failure. _internal_flash_erase discarded the return value, proceeding to erase even if the flash was not unlocked. Apply the same check for consistency and safety. Signed-off-by: Andrew Yong --- src/platform/stm32wl/LittleFS.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platform/stm32wl/LittleFS.cpp b/src/platform/stm32wl/LittleFS.cpp index 40f32eca89d..ac45473db16 100644 --- a/src/platform/stm32wl/LittleFS.cpp +++ b/src/platform/stm32wl/LittleFS.cpp @@ -130,7 +130,9 @@ static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) /* calculate the absolute page, i.e. what the ST wants */ EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE; _LFS_DBG("Erasing block %d at 0x%08x... ", block, address); - HAL_FLASH_Unlock(); + if (HAL_FLASH_Unlock() != HAL_OK) { + return LFS_ERR_IO; + } hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); HAL_FLASH_Lock(); From 29b16fed927e70322185c068922162c08993d23c Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 13:57:52 +0800 Subject: [PATCH 5/8] stm32wl: fix _internal_flash_prog to abort on first write error Previously the programming loop continued to the next doubleword after HAL_FLASH_Program() failed, potentially writing to invalid addresses and returning a misleading error code only at the end (last iteration). HAL_FLASH_Lock() was also skipped on the mid-loop early return path. - Move bounds check before the loop (validate full range at once) - Break on first HAL error so subsequent doublewords are not written - Move HAL_FLASH_Lock() after the loop so it always runs Signed-off-by: Andrew Yong --- src/platform/stm32wl/LittleFS.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/platform/stm32wl/LittleFS.cpp b/src/platform/stm32wl/LittleFS.cpp index ac45473db16..34bd1085d49 100644 --- a/src/platform/stm32wl/LittleFS.cpp +++ b/src/platform/stm32wl/LittleFS.cpp @@ -84,30 +84,29 @@ static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, l LFS_UNUSED(c); _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); + + // Validate the full write range before touching flash + lfs_block_t addr_end = address + (lfs_block_t)(dw_count * 8) - 1; + if ((address < LFS_FLASH_ADDR_BASE) || (addr_end > LFS_FLASH_ADDR_END)) { + _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x-0x%08x.\n", address, addr_end); + return LFS_ERR_INVAL; + } + if (HAL_FLASH_Unlock() != HAL_OK) { return LFS_ERR_IO; } for (uint32_t i = 0; i < dw_count; i++) { - if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { - _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address); - HAL_FLASH_Lock(); - return LFS_ERR_INVAL; - } hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); if (hal_rc != HAL_OK) { - /* Error occurred while writing data in Flash memory. - * User can add here some code to deal with this error. - */ _LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError()); + break; // Do not continue programming after a failure } address += 8; bufp += 1; } - if (HAL_FLASH_Lock() != HAL_OK) { - return LFS_ERR_IO; - } + HAL_FLASH_Lock(); - return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; } // Erase a block. A block must be erased before being programmed. From 794940e5f8e6a844671b9591aea3e17fd370acc4 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 13:59:32 +0800 Subject: [PATCH 6/8] stm32wl: clear stale flash SR error flags before erase and program Stale error flags in FLASH->SR from a previous failed operation can cause HAL_FLASH_Program() or HAL_FLASHEx_Erase() to return HAL_ERROR immediately without attempting the operation. Add __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS) after each HAL_FLASH_Unlock() in both _internal_flash_prog and _internal_flash_erase to ensure a clean state before each operation. Signed-off-by: Andrew Yong --- src/platform/stm32wl/LittleFS.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/platform/stm32wl/LittleFS.cpp b/src/platform/stm32wl/LittleFS.cpp index 34bd1085d49..c8fe272bc29 100644 --- a/src/platform/stm32wl/LittleFS.cpp +++ b/src/platform/stm32wl/LittleFS.cpp @@ -95,6 +95,7 @@ static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, l if (HAL_FLASH_Unlock() != HAL_OK) { return LFS_ERR_IO; } + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); for (uint32_t i = 0; i < dw_count; i++) { hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); if (hal_rc != HAL_OK) { @@ -132,6 +133,7 @@ static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) if (HAL_FLASH_Unlock() != HAL_OK) { return LFS_ERR_IO; } + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); HAL_FLASH_Lock(); From 7d584247b75edb9a2a556a39363441afce0828a6 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 18:06:15 +0800 Subject: [PATCH 7/8] stm32wl: reject flash prog writes not aligned to 8-byte doubleword The STM32WL HAL minimum write unit is one 64-bit doubleword (8 bytes). _internal_flash_prog silently truncated any trailing bytes when size % 8 != 0 because dw_count = size / 8 drops the remainder. Return LFS_ERR_INVAL early so LittleFS sees the error rather than a silent short write. Signed-off-by: Andrew Yong --- src/platform/stm32wl/LittleFS.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/platform/stm32wl/LittleFS.cpp b/src/platform/stm32wl/LittleFS.cpp index c8fe272bc29..1736d0907f5 100644 --- a/src/platform/stm32wl/LittleFS.cpp +++ b/src/platform/stm32wl/LittleFS.cpp @@ -83,6 +83,11 @@ static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, l LFS_UNUSED(c); + // STM32WL HAL minimum write unit is one 64-bit doubleword (8 bytes) + if (size % 8 != 0) { + return LFS_ERR_INVAL; + } + _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); // Validate the full write range before touching flash From feee3fd8e07340acf4c58f4a51da53a3d0372e46 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Apr 2026 20:49:19 +0800 Subject: [PATCH 8/8] stm32wl: exclude backup preferences via MESHTASTIC_EXCLUDE_BACKUP backup.proto requires 2 LittleFS blocks (4 KB) and is only written on an explicit admin command. On a flash-constrained node with 12 usable blocks, this is headroom better preserved for normal operation. Guard the three AdminModule case handlers and NodeDB declarations/implementations behind #if !MESHTASTIC_EXCLUDE_BACKUP, matching the pattern used by existing MESHTASTIC_EXCLUDE_* flags throughout the codebase. Signed-off-by: Andrew Yong --- src/mesh/NodeDB.cpp | 2 ++ src/mesh/NodeDB.h | 10 ++++++++-- src/modules/AdminModule.cpp | 2 ++ variants/stm32/stm32.ini | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8a665c8614f..cf77e5beb35 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -2190,6 +2190,7 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub } #endif +#if !MESHTASTIC_EXCLUDE_BACKUP bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { bool success = false; @@ -2277,6 +2278,7 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, #endif return success; } +#endif // !MESHTASTIC_EXCLUDE_BACKUP /// Record an error that should be reported via analytics void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index f6be963c184..b73c786fc2f 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -102,7 +102,9 @@ static constexpr const char *configFileName = "/prefs/config.proto"; static constexpr const char *uiconfigFileName = "/prefs/uiconfig.proto"; static constexpr const char *moduleConfigFileName = "/prefs/module.proto"; static constexpr const char *channelFileName = "/prefs/channels.proto"; +#if !MESHTASTIC_EXCLUDE_BACKUP static constexpr const char *backupFileName = "/backups/backup.proto"; +#endif /// Given a node, return how many seconds in the past (vs now) that we last heard from it uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n); @@ -316,9 +318,11 @@ class NodeDB bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); #endif +#if !MESHTASTIC_EXCLUDE_BACKUP bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); +#endif /// Notify observers of changes to the DB void notifyObservers(bool forceUpdate = false) @@ -331,9 +335,11 @@ class NodeDB private: bool duplicateWarned = false; bool localPositionUpdatedSinceBoot = false; - uint32_t lastNodeDbSave = 0; // when we last saved our db to flash + uint32_t lastNodeDbSave = 0; // when we last saved our db to flash +#if !MESHTASTIC_EXCLUDE_BACKUP uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually - uint32_t lastSort = 0; // When last sorted the nodeDB +#endif + uint32_t lastSort = 0; // When last sorted the nodeDB /// Find a node in our DB, create an empty NodeInfoLite if missing meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 05bc0aa5d84..c06a39e840b 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -499,6 +499,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #endif break; } +#if !MESHTASTIC_EXCLUDE_BACKUP case meshtastic_AdminMessage_backup_preferences_tag: { LOG_INFO("Client requesting to backup preferences"); if (nodeDB->backupPreferences(r->backup_preferences)) { @@ -535,6 +536,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #endif break; } +#endif // !MESHTASTIC_EXCLUDE_BACKUP case meshtastic_AdminMessage_send_input_event_tag: { LOG_INFO("Client requesting to send input event"); handleSendInputEvent(r->send_input_event); diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index 542d0880065..515608b51e3 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -25,6 +25,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_BLUETOOTH=1 -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. + -DMESHTASTIC_EXCLUDE_BACKUP=1 ; Backup preferences require 2 flash blocks (4 KB); not useful on a flash-constrained node. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized.