Skip to content

fix(stm32wl,nrf52,fs): flash hardening, FS platform unification, write-behind LFS cache (FORMAT BREAK)#10171

Open
ndoo wants to merge 16 commits intomeshtastic:developfrom
mesh-malaysia:feat/stm32-lfs-cleanup
Open

fix(stm32wl,nrf52,fs): flash hardening, FS platform unification, write-behind LFS cache (FORMAT BREAK)#10171
ndoo wants to merge 16 commits intomeshtastic:developfrom
mesh-malaysia:feat/stm32-lfs-cleanup

Conversation

@ndoo
Copy link
Copy Markdown
Contributor

@ndoo ndoo commented Apr 15, 2026

Summary

Three parts, ordered from least to most disruptive:

  1. FS platform unification: close NRF52 + STM32WL gaps vs ESP32, then delete the #ifdef chains
  2. STM32WL flash driver hardening: 4 silent-failure bugs, fixed by design in the write-behind cache rewrite (Part 3)
  3. STM32WL write-behind page cache + FS reservation reduction (FORMAT BREAK): virtual 256B blocks, 14 KiB reservation

Each part is independently reviewable. Builds verified after each commit.


Part 1 — FS platform unification

What was different

Behaviour ESP32 NRF52 STM32WL
SafeFile uses .tmp/readback/rename ✗ (direct write bypass)
saveToDisk format-on-retry
renameFile uses FSCom.rename() ✗ (copy+delete) ✗ (copy+delete)
File default constructor
listDir(del=true) deletes files ✗ (only logged) ✗ (only logged)

Changes

File Change
src/SafeFile.cpp Remove NRF52 direct-write bypass — NRF52 was skipping .tmp/readback/rename and writing directly to the final filename
src/mesh/NodeDB.cpp Enable format-on-retry for all platforms via new fsFormat() helper; remove #if ARCH_NRF52 guard
src/FSCommon.cpp Add fsFormat() (portable FSCom.format() wrapper, Portduino-aware); unify renameFile() to use FSCom.rename() on all platforms; consolidate listDir(), getFiles(), rmDir() with a single filepath pointer — ~18 arch-specific guards removed
src/modules/AdminModule.cpp Gate handleStoreDeviceUIConfig behind #if HAS_SCREEN — no benefit persisting UI config on screen-less devices
src/platform/stm32wl/STM32_LittleFS_File.h/.cpp Add File() default constructor — prerequisite for unified xmodem.h
src/xmodem.h File file; — no arch guard needed
variants/nrf52840/nrf52.ini Temporarily pin to mesh-malaysia/Adafruit_nRF52_Arduino SHA with NRF52 File() default constructor (pending meshtastic/Adafruit_nRF52_Arduino#5)

Behavior unchanged for ESP32, RP2040, Portduino — only NRF52/STM32WL code paths change to match what other platforms already do.


Part 2 — STM32WL flash driver hardening

Four bugs in the original driver caused silent flash corruption. All are fixed by design in the write-behind cache rewrite (Part 3), which concentrates all HAL I/O in _flash_cache_flush():

Bug Fix in _flash_cache_flush()
HAL_FLASH_Unlock() return unchecked in erase — always proceeded even if unlock failed Returns LFS_ERR_IO if unlock fails
HAL_FLASH_Program errors ignored — loop continued through remaining doublewords Aborts loop on first error; checks lock return too
Stale FLASH SR error flags not cleared — HAL rejects subsequent operations __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS) before erase
Unaligned writes silently accepted — STM32WL requires 8-byte doubleword alignment Impossible: always programs full 2048B pages via uint64_t*

Part 3 — STM32WL write-behind page cache (FORMAT BREAK)

Adds a RMW page cache (same approach as NRF52 Adafruit flash_cache.c, adapted for the 2048B STM32WL physical page). LFS uses 256-byte virtual blocks; erase/prog calls accumulate in the RAM cache and the physical page is flushed on sync or when a different page is addressed.

Parameter Before After
block_size 2048 B (= physical page) 256 B (virtual; cache RMWs physical pages)
block_count 14 (28 KiB) 56 (7 physical pages = 14 KiB)
Max fragmentation/file 2047 B 255 B
Heap per open LFS file 4 KB 512 B
Code flash freed (all STM32WL variants) +14 KB

FS reservation applied to all STM32WL variants via board_upload.maximum_size: wio-e5, rak3172, russell, CDEBYTE_E77-MBL, milesight_gs301.

This is a LFS superblock parameter change — existing devices will reformat on first boot and lose stored config. The flash driver bugs fixed in Part 2 very likely caused silent corruption on deployed devices; the format break is justified and should be documented in release notes.


Build verification

Variant v2.7.22 (01bd4cfb) This PR (3dc3948a)
wio-e5 97.7% (228132 / 233472 B) ✅ 93.1% (230620 / 247808 B)
rak3172 74.3% (173428 / 233472 B) ✅ 72.1% (178700 / 247808 B)
rak4631 ✅ SUCCESS
heltec-v3 ✅ SUCCESS
pico ✅ SUCCESS

Dependencies

🤖 Generated with Claude Code

@github-actions github-actions Bot added the bugfix Pull request that fixes bugs label Apr 15, 2026
@thebentern thebentern requested review from Stary2001, caveman99 and Copilot and removed request for caveman99 April 15, 2026 12:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens flash/filesystem behavior across STM32WL and nRF52 to match ESP32-style atomic writes, removes several arch-specific filesystem conditionals, and introduces a STM32WL LittleFS write-behind cache to support smaller virtual block sizes (format-breaking).

Changes:

  • Harden STM32WL flash/LittleFS handling (error-flag clearing, stricter write behavior) and reduce reserved FS space for wio-e5.
  • Unify FS behavior across platforms (atomic SafeFile rename path, universal format-on-retry, shared FSCommon/xmodem code).
  • Add default-constructible File on STM32WL (and switch nRF52 framework to a fork pending upstream support).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
variants/stm32/wio-e5/platformio.ini Adjust upload maximum size to reflect reduced FS reservation (20KB).
variants/stm32/stm32.ini Disable backup preferences on flash-constrained STM32 builds.
variants/nrf52840/nrf52.ini Temporarily point nRF52 framework to fork to obtain File() default constructor.
src/xmodem.h Remove arch-conditional File initialization now that File() exists.
src/platform/stm32wl/STM32_LittleFS_File.h Add File() default constructor declaration.
src/platform/stm32wl/STM32_LittleFS_File.cpp Implement File() default constructor binding to InternalFS.
src/platform/stm32wl/LittleFS.cpp Introduce write-behind page cache and change LFS tunables (format break).
src/modules/AdminModule.cpp Gate UI config persistence to screen-capable builds; gate backup admin messages.
src/mesh/NodeDB.h Compile-time exclusion of backup APIs/paths when backup is disabled.
src/mesh/NodeDB.cpp Enable format-on-retry across all platforms; compile-time exclusion of backup impl.
src/SafeFile.cpp Remove nRF52 direct-write bypass; use temp+readback+rename path.
src/FSCommon.cpp Remove arch-specific FS behavior branches; unify path handling and recursive delete.

Comment thread variants/nrf52840/nrf52.ini Outdated
Comment thread src/platform/stm32wl/LittleFS.cpp
Comment thread src/FSCommon.cpp
Comment thread src/FSCommon.cpp
Comment thread src/FSCommon.cpp
Comment thread src/FSCommon.cpp
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens and unifies filesystem behavior across STM32WL/NRF52/other platforms, and introduces a format-breaking STM32WL LittleFS optimization (write-behind page cache + smaller virtual blocks) along with variant config updates to reclaim flash space.

Changes:

  • Updates STM32WL LittleFS to use a write-behind page cache, reducing LittleFS virtual block size and FS reservation (format break).
  • Removes architecture-specific filesystem divergences (SafeFile atomic write path, FSCommon cleanup, xmodem File default construction) and enables format-on-retry broadly.
  • Adds build/config adjustments: STM32 maximum_size bump, optional exclusion of backup preferences, and a temporary NRF52 framework fork for File() default ctor support.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
variants/stm32/wio-e5/platformio.ini Bumps upload maximum_size to reflect reduced FS reservation.
variants/stm32/stm32.ini Excludes backup preferences to save flash on STM32 targets.
variants/stm32/russell/platformio.ini Bumps upload maximum_size to reflect reduced FS reservation.
variants/stm32/rak3172/platformio.ini Bumps upload maximum_size to reflect reduced FS reservation.
variants/stm32/milesight_gs301/platformio.ini Bumps upload maximum_size to reflect reduced FS reservation.
variants/stm32/CDEBYTE_E77-MBL/platformio.ini Bumps upload maximum_size to reflect reduced FS reservation.
variants/nrf52840/nrf52.ini Temporarily points NRF52 framework to a fork for File() default constructor support.
src/xmodem.h Removes arch-specific File initialization by relying on default constructor.
src/platform/stm32wl/STM32_LittleFS_File.h Declares a default File() constructor for STM32WL LittleFS File.
src/platform/stm32wl/STM32_LittleFS_File.cpp Implements default File() constructor binding to InternalFS.
src/platform/stm32wl/LittleFS.cpp Implements write-behind page cache and new LittleFS tunables (format break).
src/modules/AdminModule.cpp Gates UI config persistence behind HAS_SCREEN; gates backup admin commands behind MESHTASTIC_EXCLUDE_BACKUP.
src/mesh/NodeDB.h Compiles out backup-related APIs/state when MESHTASTIC_EXCLUDE_BACKUP is set.
src/mesh/NodeDB.cpp Enables format-on-retry path universally; compiles out backup implementation when excluded.
src/SafeFile.cpp Removes NRF52 direct-write bypass so all platforms use tmp+readback+rename path.
src/FSCommon.cpp Simplifies rename/list/delete logic and reduces arch-specific handling.

Comment thread src/FSCommon.cpp Outdated
Comment thread src/FSCommon.cpp
Comment thread variants/nrf52840/nrf52.ini Outdated
@Stary2001
Copy link
Copy Markdown
Member

Part 1 flash driver changes are fine with a couple comments.
955621e is fine? Even more ifdefs though.
Part 2 is fine, but need to test on NRF52 due to 9cdfa33.
Part 3 is probably fine, it booted and saved when I tested it. I'm still not quite sure how it stacks up for flash wear, but maybe if it can now pack all the configs into the same physical flash page it means less erasing.

Copy link
Copy Markdown
Member

@Stary2001 Stary2001 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the HAL_FLASH_Lock fix and that should be it.

Comment thread src/platform/stm32wl/LittleFS.cpp
Comment thread src/platform/stm32wl/LittleFS.cpp Outdated
@Stary2001
Copy link
Copy Markdown
Member

@Stary2001
Copy link
Copy Markdown
Member

Stary2001 commented Apr 16, 2026

CI fails for native:

#13 193.3 src/mesh/NodeDB.cpp: In member function 'bool NodeDB::saveToDisk(int)':
#13 193.3 src/mesh/NodeDB.cpp:1608:15: error: 'class fs::FS' has no member named 'format'
#13 193.3  1608 |         FSCom.format();
#13 193.3       |               ^~~~~~
#13 193.5 Compiling .pio/build/native-tft/src/mesh/PhoneAPI.cpp.o
#13 193.9 Compiling .pio/build/native-tft/src/mesh/ProtobufModule.cpp.o
#13 193.9 *** [.pio/build/native-tft/src/mesh/NodeDB.cpp.o] Error 1
#13 197.2 ========================= [FAILED] Took 111.73 seconds =========================```

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

CI fails for native:


#13 193.3 src/mesh/NodeDB.cpp: In member function 'bool NodeDB::saveToDisk(int)':

#13 193.3 src/mesh/NodeDB.cpp:1608:15: error: 'class fs::FS' has no member named 'format'

#13 193.3  1608 |         FSCom.format();

#13 193.3       |               ^~~~~~

#13 193.5 Compiling .pio/build/native-tft/src/mesh/PhoneAPI.cpp.o

#13 193.9 Compiling .pio/build/native-tft/src/mesh/ProtobufModule.cpp.o

#13 193.9 *** [.pio/build/native-tft/src/mesh/NodeDB.cpp.o] Error 1

#13 197.2 ========================= [FAILED] Took 111.73 seconds =========================```

I will look at this later - needs fixing

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

Depends on meshtastic/Adafruit_nRF52_Arduino#4, meshtastic/Adafruit_nRF52_Arduino#5

Wait, which branch was I supposed to target?

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

Part 1 flash driver changes are fine with a couple comments.

Inline comments addressed — replied and resolved in each thread. Summary:

  • HAL_FLASH_Lock() return value now checked and propagated in _flash_cache_flush
  • alignas(8) added to _page_cache to fix UB on the const uint64_t* cast
  • strncpy/strcpy safety issues in FSCommon.cpp fixed with bounded copies + explicit NUL termination
  • nrf52.ini branch ref pinned to commit SHA; commit carries DO NOT MERGE: title and FOLLOW-UP REQUIRED note referencing nrf52(fs): add File() default constructor bound to InternalFS Adafruit_nRF52_Arduino#5

Also fixed a CI failure (native-tft): fs::FS has no format() method on Portduino. Introduced fsFormat() in FSCommon — dispatches to FSCom.format() on embedded targets and rmDir("/prefs") + FSBegin() on Portduino (both primitives already used unconditionally on Portduino in factoryReset()). Portduino/native build testers, please verify the format-on-retry path.

955621e is fine? Even more ifdefs though.

Fair observation. The MESHTASTIC_EXCLUDE_BACKUP ifdefs are a deliberate compile-time exclusion rather than a runtime guard — the goal is to not pay the flash cost at all on STM32 targets that are close to the ceiling (wio-e5 was at ~94% before this series). A runtime check would still link the backup code. If there's a preferred pattern for capability exclusion in this codebase I'm happy to follow it.

Part 2 is fine, but need to test on NRF52 due to 9cdfa33.

Agreed — 9cdfa332 removes the NRF52 direct-write bypass in SafeFile and routes NRF52 through the same .tmp → readback → rename path used by other platforms. I don't have NRF52 hardware to test with. NRF52 testers, please exercise file save/load across a reboot to confirm no regression.

Part 3 is probably fine, it booted and saved when I tested it. I'm still not quite sure how it stacks up for flash wear, but maybe if it can now pack all the configs into the same physical flash page it means less erasing.

Your intuition is right. With 256-byte virtual blocks, LFS is much more likely to pack the small meshtastic config files (most are well under 256 bytes) into the same physical page. When LFS writes multiple files that share a physical page, the write-behind cache batches them — the physical erase+reprogram is deferred until sync or page eviction, so co-located updates cost one physical erase instead of N. Worst case (every virtual block on a different physical page) is the same number of physical erases as the old 2048-byte block scheme. Wear levelling also has more room to work with 80 blocks vs the old 14.

Geez, I hate when Claude decides to post something without checking with me first.

ndoo added 4 commits April 17, 2026 02:32
_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 <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

Ok, here comes a real human reply (KILL ALL HUMANS…)

Part 1 flash driver changes are fine with a couple comments. [955621e]

I think these will be resolved shortly…

(955621e) is fine? Even more ifdefs though.

Actually, now that we have smaller virtual blocks, we actually have space for backup, so I will drop the commit entirely.

Part 2 is fine, but need to test on NRF52 due to 9cdfa33.

I’ve been testing the integration branch on a T-Echo, but yes it does need more testing.

Part 3 is probably fine, it booted and saved when I tested it. I'm still not quite sure how it stacks up for flash wear, but maybe if it can now pack all the configs into the same physical flash page it means less erasing.

Probably… we should probably watch the utilization and consider reducing the allocation more…

ndoo added 5 commits April 17, 2026 02:41
NRF52 was bypassing the .tmp/readback/rename path entirely — openFile()
deleted the target file and wrote directly to it, and close() returned
true without verifying the write or renaming anything.

Adafruit_LittleFS::rename() calls lfs_rename() directly (confirmed at
Adafruit_LittleFS.cpp:205). Remove both ARCH_NRF52 guards so NRF52
follows the same write-to-.tmp → readback-hash → rename path used by
all other platforms.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
handleStoreDeviceUIConfig() was writing /prefs/uiconfig.proto
unconditionally. MenuHandler.cpp is already gated behind #if HAS_SCREEN,
so there is no path that populates UI config on screen-less platforms.
Guard the save with #if HAS_SCREEN to avoid wasting a flash block on
devices that will never use it.

The read path (handleGetDeviceUIConfig) does not touch the filesystem
and needs no change.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The FSCom.format() call on save failure was guarded to ARCH_NRF52 with
a comment that other platforms were not ready (bug meshtastic#4184). STM32WL was
added to the guard in a prior commit. All platforms now expose format
semantics and the retry logic is identical — remove the guard.

To keep NodeDB.cpp platform-agnostic and fix a CI failure on native-tft
(portduino's fs::FS has no format() method), introduce fsFormat() in
FSCommon as the single call-site for all callers:

  - Embedded (ESP32, NRF52, STM32WL, RP2040): delegates to FSCom.format()
  - Portduino: rmDir("/prefs") + FSBegin() (a no-op on portduino).
    rmDir("/prefs") is already called unconditionally by factoryReset()
    (NodeDB.cpp:504), so both primitives are proven on portduino.

Replace both direct FSCom.format() calls in NodeDB.cpp with fsFormat().

Note: we do not run portduino locally — portduino/native build testers
please verify the format-on-retry path.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…rnalFS

Adds File() to the Adafruit LittleFS File class (in the Meshtastic
Adafruit_nRF52_Arduino fork), delegating to File(InternalFS). This
matches the default-constructible File API on all other platforms.

The constructor is implemented in Adafruit_LittleFS_File.cpp rather
than inline in the header to avoid a circular include between
Adafruit_LittleFS_File.h and InternalFileSystem.h.

FOLLOW-UP REQUIRED: nrf52.ini points to a commit SHA on the
mesh-malaysia/Adafruit_nRF52_Arduino fork instead of the upstream
meshtastic framework. Once meshtastic/Adafruit_nRF52_Arduino#5 is
merged, revert nrf52.ini to point back to the upstream meshtastic
framework URL.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds File() to STM32_LittleFS_Namespace::File, delegating to
File(InternalFS). Implemented in the .cpp to avoid a circular include
between STM32_LittleFS_File.h (which cannot include LittleFS.h) and
the InternalFS extern declaration.

This matches the File API on ESP32/RP2040/Portduino and is a
prerequisite for removing the ARCH_STM32WL guard in xmodem.h.

No behavior change — the constructor leaves the file in the same
closed/unattached state as File(InternalFS) would.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
ndoo added 2 commits April 17, 2026 02:41
Now that NRF52 and STM32WL have File() default constructors and NRF52
has working atomic SafeFile rename, the capability gaps are closed.
Remove all per-arch guards across the shared FS layer:

FSCommon.cpp — renameFile():
  Use FSCom.rename() on all platforms. Adafruit_LittleFS::rename()
  calls lfs_rename() directly (Adafruit_LittleFS.cpp:205). The
  copy+delete fallback on NRF52/RP2040 was never necessary.

FSCommon.cpp — getFiles():
  Replace four ARCH_ESP32 guards with a single filepath pointer at
  the top of the loop (file.path() on ESP32, file.name() elsewhere).
  Fix strcpy(fileInfo.file_name, filepath): bounded to
  sizeof(fileInfo.file_name)-1 with explicit NUL termination to prevent
  overflow of the 228-byte meshtastic_FileInfo::file_name array.

FSCommon.cpp — listDir():
  Same filepath pointer approach. NRF52/STM32WL were in an else-branch
  that only logged but never deleted — now all platforms follow the
  unified del path. 12 guards → 2.
  Fix three strncpy(buffer, ..., sizeof(buffer)) calls that did not
  NUL-terminate when source length >= sizeof(buffer) (255 bytes).
  Add explicit buffer[sizeof(buffer)-1] = '\0' after each.

FSCommon.cpp — rmDir():
  Use listDir(del=true) everywhere. The ARCH_NRF52 rmdir_r() path and
  the ARCH_ESP32|RP2040|PORTDUINO listDir() path collapse to one line.

SafeFile.cpp:
  ARCH_NRF52 bypass removed (handled in preceding commit).

xmodem.h:
  File file; now works on all platforms via default constructors
  added in the two preceding commits.

Remaining #ifdef ARCH_ESP32 in FSCommon.cpp: exactly 4, all for the
file.path() vs file.name() API difference (ESP32 Arduino LittleFS
returns the full path; all others return only the name). That
difference lives in the framework and cannot be closed here.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nd FS reservation (FORMAT BREAK)

Adds a write-behind (RMW) page cache to the STM32WL LittleFS driver,
modelled after the NRF52 Adafruit approach (flash_cache.c). This allows
LFS to use 256-byte virtual blocks backed by 2048-byte physical pages:
the erase/prog callbacks accumulate changes in a 2 KB RAM buffer; the
sync callback (and page eviction on page-change) flushes with a single
HAL physical-erase + doubleword-program pass.

LFS tunables changed (FORMAT BREAK — superblock parameters):
  block_size:  2048 B → 256 B  (8 virtual blocks per physical page)
  read_size:   2048 B → 256 B  (= block_size)
  prog_size:   2048 B → 256 B  (= block_size; hardware min is 8 B)
  block_count: 112   → 80     (14 phys pages → 10 phys pages = 20 KiB)

Benefits:
  - Internal fragmentation: max 2047 B/file → max 255 B/file
  - Heap per open LFS file: ~4 KB → 512 B (prog + read buffers)
  - Code flash headroom: 6.7 KB → ~14.1 KB (+7.4 KB)
  - Block budget: 80 virtual blocks, worst-case peak ~20, ~60 free

Updates board_upload.maximum_size in wio-e5/platformio.ini from 233472
(256 KB − 28 KB) to 241664 (256 KB − 20 KB) to match the reduced FS
reservation.

Justification for the format break: the prior STM32WL firmware had
several flash write bugs fixed earlier in this series (missing error
flag clearing, no abort on first write failure, unaligned write
acceptance). These bugs very likely caused silent config corruption on
deployed devices. The format break should be treated as an enhancement:
it provides a clean, reliably-written starting point. Users will need
to reconfigure their device once after this update.

Correctness fixes applied to the cache implementation:
  - alignas(8) on _page_cache: the buffer was uint8_t[] (alignment 1)
    but _flash_cache_flush casts it to const uint64_t* — undefined
    behaviour per C++ standard, potential Cortex-M hardfault. alignas(8)
    guarantees the required alignment for the doubleword cast.
  - HAL_FLASH_Lock() return value: was discarded. Now assigned to
    lock_rc and propagated into rc if prior writes succeeded, so LFS
    sees the error rather than a false success.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@ndoo ndoo force-pushed the feat/stm32-lfs-cleanup branch from cac0908 to f5f29f3 Compare April 16, 2026 18:43
@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

CI fails for native:

#13 193.3 src/mesh/NodeDB.cpp: In member function 'bool NodeDB::saveToDisk(int)':
#13 193.3 src/mesh/NodeDB.cpp:1608:15: error: 'class fs::FS' has no member named 'format'
#13 193.3  1608 |         FSCom.format();
#13 193.3       |               ^~~~~~
#13 193.5 Compiling .pio/build/native-tft/src/mesh/PhoneAPI.cpp.o
#13 193.9 Compiling .pio/build/native-tft/src/mesh/ProtobufModule.cpp.o
#13 193.9 *** [.pio/build/native-tft/src/mesh/NodeDB.cpp.o] Error 1
#13 197.2 ========================= [FAILED] Took 111.73 seconds =========================```

I had CC implement this for Portduino in 5623be6

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

Will update the PR text later, but now is a good time to request a Copilot re-review.

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 16, 2026

PR text updated.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens STM32WL flash/LittleFS behavior, unifies filesystem feature parity across architectures (reducing arch-specific #ifdef paths), and introduces a format-breaking STM32WL LittleFS optimization using a write-behind page cache plus smaller virtual blocks/reservation.

Changes:

  • Reduce STM32WL filesystem reservation and adjust LittleFS tunables to use a write-behind page cache with 256-byte virtual blocks (format-breaking).
  • Unify FS behaviors across platforms: atomic SafeFile usage on nRF52, shared fsFormat() helper, and simplified FSCommon implementations.
  • Add default File() constructor support for STM32WL and simplify XModem adapter file handling; gate UI config persistence on HAS_SCREEN.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
variants/stm32/wio-e5/platformio.ini Adjust max firmware size to reserve 20KB for FS (down from 28KB).
variants/stm32/russell/platformio.ini Same FS reservation adjustment.
variants/stm32/rak3172/platformio.ini Same FS reservation adjustment.
variants/stm32/milesight_gs301/platformio.ini Same FS reservation adjustment.
variants/stm32/CDEBYTE_E77-MBL/platformio.ini Same FS reservation adjustment.
variants/nrf52840/nrf52.ini Pin Adafruit nRF52 framework to a specific fork SHA for File() default ctor dependency.
src/xmodem.h Remove arch-specific File construction; rely on default ctor.
src/platform/stm32wl/STM32_LittleFS_File.h Declare STM32WL File() default constructor binding to InternalFS.
src/platform/stm32wl/STM32_LittleFS_File.cpp Implement STM32WL File() default constructor.
src/platform/stm32wl/LittleFS.cpp Implement STM32WL write-behind page cache + smaller virtual block size and updated mount/format behavior.
src/modules/AdminModule.cpp Avoid saving UI config on builds without screens (#if HAS_SCREEN).
src/mesh/NodeDB.cpp Use new fsFormat() helper for reset/retry formatting paths.
src/SafeFile.cpp Remove nRF52 direct-write bypass; use standard atomic temp+readback+rename path.
src/FSCommon.h Add fsFormat() declaration.
src/FSCommon.cpp Simplify renameFile(), add fsFormat(), and unify list/remove logic across architectures.

Comment thread src/FSCommon.cpp
ndoo added 2 commits April 25, 2026 09:46
…REAK)

Reduces LFS_FLASH_TOTAL_SIZE from 10 × 2 KiB pages (20 KiB) to
7 × 2 KiB pages (14 KiB), freeing 6 KiB for firmware.

board_upload.maximum_size updated accordingly across all STM32WL variants:
  241664 (256 KiB - 20 KiB) → 247808 (256 KiB - 14 KiB)

This is a FORMAT BREAK: existing filesystems must be erased before use.

Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
Avoids undefined behavior and -Wreturn-type warnings in configurations
that compile FSCommon.cpp without a filesystem backend.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 25, 2026

  1. Reduced STM32WL FS reservation

STM32WL LittleFS: 7-page (14 KiB) reservation safety analysis

LFS_BLOCK_SIZE = 256 B · MAX_NUM_NODES = 10 · 1 physical page = 8 virtual blocks

Component vblocks Notes
config.proto (754 B) 3
module_config.proto (820 B) 4
devicestate.proto (1,737 B) 7
channels.proto (718 B) 3
nodes.proto (1,962 B) 8 2 + 10 × NodeInfoLite (196 B)
transmit_history.dat (118 B) 1 6 B header + 16 × 7 B entries
uiconfig.proto 0 skipped — #if HAS_SCREEN
LFS metadata ~9 superblock pair + 2 dir pairs + CTZ
devicestate .tmp peak +7 fullAtomic=true — old + new coexist
Worst-case peak ~42 / 56 75% — 3.5 KiB margin

This gives us a little bit of space back for code.

  1. Fix for Copilot review

@ndoo ndoo changed the title fix(stm32wl,nrf52,fs): flash driver hardening, FS parity cleanup, write-behind cache fix(stm32wl,nrf52,fs): flash hardening, FS platform unification, write-behind LFS cache (FORMAT BREAK) Apr 25, 2026
@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 25, 2026

PR title updated.

Also added a FLASH usage summary:

Variant v2.7.22 (01bd4cfb) This PR (3dc3948a)
wio-e5 97.7% (228132 / 233472 B) ✅ 93.1% (230620 / 247808 B)
rak3172 74.3% (173428 / 233472 B) ✅ 72.1% (178700 / 247808 B)

@thebentern thebentern requested a review from Stary2001 April 25, 2026 11:01
Copy link
Copy Markdown
Member

@Stary2001 Stary2001 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm but merge the nrf pr first and get @ndoo to rebase out the do not merge committ

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 26, 2026

meshtastic/Adafruit_nRF52_Arduino#4 is merged but meshtastic/Adafruit_nRF52_Arduino#5 needs to be merged too; edit: because the lib_deps pulls from the cpp17 branch

@thebentern
Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

@ndoo
Copy link
Copy Markdown
Contributor Author

ndoo commented Apr 30, 2026

@copilot resolve the merge conflicts in this pull request

I can do this as part of a bigger rebase on head of develop if there's any merge conflicts.

But first please help to merge that the cpp17 branch of the other PR (sorry, on my phone so a bit of a hassle to copy the link)

@Stary2001
Copy link
Copy Markdown
Member

It's merged already

@thebentern
Copy link
Copy Markdown
Contributor

Copilot is bad about not obeying me on other folk's remotes anyway 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants