Skip to content

Commit 3cb7d66

Browse files
committed
Enhance filesystem formatting support and tests
- Added support for `mimxrt` to the `--format` option in `mpflash flash`. - Updated `CHANGELOG.md` to reflect the new filesystem support. - Improved the `erase_bdev.py` and `format_bdev.py` scripts for better runtime detection of filesystem types. - Added unit tests for filesystem class detection and block device polling. Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
1 parent 88a0310 commit 3cb7d66

6 files changed

Lines changed: 213 additions & 68 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ All notable changes to mpflash are documented in this file.
88

99
- **`--format` option for `mpflash flash`** — reformats the board's filesystem after
1010
flashing, recreating an empty filesystem of the same type (`VfsLfs2` or `VfsFat`) via
11-
the MicroPython block device. Supported on `rp2`, `esp32`, `esp8266`, `samd`, `stm32`
12-
and `nrf`.
11+
the MicroPython block device. Supported on `rp2`, `esp32`, `esp8266`, `samd`, `stm32`,
12+
`nrf` and `mimxrt`. The mount point and filesystem type are detected at runtime using
13+
the same `vfs.mount()` enumeration as `mpremote df`.
1314
- **`mpflash format` command** — reformats the filesystem of connected boards without
1415
flashing new firmware. Asks for confirmation before erasing (skip with `--yes`) and
1516
supports the same ports as `flash --format`.

mpflash/flash/format_fs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
FORMAT_SCRIPT = HERE / "format_bdev.py"
1616

1717
# Ports for which the internal-flash block device is known.
18-
SUPPORTED_FORMAT_PORTS = frozenset({"rp2", "esp32", "esp8266", "samd", "stm32", "nrf"})
18+
SUPPORTED_FORMAT_PORTS = frozenset({"rp2", "esp32", "esp8266", "samd", "stm32", "nrf", "mimxrt"})
1919

2020
_OK_MARKER = "FORMAT: done"
2121

mpflash/mpremoteboard/erase_bdev.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# pragma: no cover
22
"""Erase the filesystem block device, then reset the board.
33
4-
Run on the board via ``mpremote run``. It locates the port's filesystem
5-
``bdev`` (the same one ``_boot.py`` mounts), unmounts it, erases every block
6-
using the extended block-device protocol and finally calls ``machine.reset()``.
7-
The board reboots into MicroPython and ``_boot.py`` recreates a fresh, empty
8-
filesystem.
4+
Run on the board via ``mpremote run``. It locates the port's block device
5+
(polling the factory each MicroPython port exposes), unmounts every writable
6+
filesystem found via ``vfs.mount()`` (the same enumeration as ``mpremote df``),
7+
erases every block using the extended block-device protocol and finally calls
8+
``machine.reset()``. The board reboots into MicroPython and ``_boot.py``
9+
recreates a fresh, empty filesystem.
910
1011
Only the filesystem storage region is erased - the firmware itself lives in a
1112
separate flash region and is left untouched. Entering the UF2 bootloader is a
@@ -17,44 +18,68 @@
1718
_IOCTL_BLOCK_ERASE = 6
1819

1920

20-
def _get_bdev():
21-
"""Return (bdev, mount_point) for the running port, or (None, None)."""
21+
def _vfs():
22+
"""Return the module providing ``mount``/``umount`` (``vfs`` or legacy ``os``)."""
2223
try:
23-
import rp2
24+
import vfs
2425

25-
return rp2.Flash(), "/"
26-
except Exception:
27-
pass
28-
try:
29-
import samd
26+
return vfs
27+
except ImportError:
28+
import os
3029

31-
return samd.Flash(), "/"
32-
except Exception:
33-
pass
30+
return os
31+
32+
33+
# (module, attribute, kwargs): the block-device factory each MicroPython port
34+
# exposes. Instantiated as ``module.attribute(**kwargs)``. Adding a new port is
35+
# a new entry here, not a port-name check elsewhere.
36+
_BDEV_FACTORIES = (
37+
("rp2", "Flash", {}),
38+
("samd", "Flash", {}),
39+
("nrf", "Flash", {}),
40+
("mimxrt", "Flash", {}),
41+
("alif", "Flash", {}),
42+
("pyb", "Flash", {"start": 0}), # stm32
43+
("psoc_edge", "QSPI_Flash", {}),
44+
)
45+
46+
47+
def _get_bdev():
48+
"""Return the internal-flash block device for the running port, or None.
49+
50+
Polls the known block-device factories in turn; the first that imports and
51+
instantiates wins. esp32 / esp8266 instead expose a ready-made ``bdev``.
52+
"""
53+
for mod_name, attr, kwargs in _BDEV_FACTORIES:
54+
try:
55+
return getattr(__import__(mod_name), attr)(**kwargs)
56+
except Exception:
57+
pass
3458
try:
35-
import nrf
59+
from flashbdev import bdev # esp32 / esp8266
3660

37-
return nrf.Flash(), "/flash"
61+
return bdev
3862
except Exception:
3963
pass
40-
return None, None
64+
return None
4165

4266

43-
def _umount(mount_point):
44-
"""Unmount the filesystem so erasing does not fight cached writes."""
67+
def _list_mounts(vfs):
68+
"""Return [(fs, mount_point), ...] via ``vfs.mount()`` (like ``mpremote df``)."""
4569
try:
46-
import vfs
70+
return list(vfs.mount())
71+
except (AttributeError, TypeError):
72+
return []
4773

48-
umount = vfs.umount
49-
except Exception:
50-
import os
5174

52-
umount = os.umount
53-
for point in (mount_point, "/", "/flash"):
75+
def _umount(vfs):
76+
"""Unmount writable filesystems so erasing does not fight cached writes."""
77+
points = [point for fs, point in _list_mounts(vfs) if "Rom" not in str(fs)]
78+
for point in points + ["/", "/flash"]:
5479
if not point:
5580
continue
5681
try:
57-
umount(point)
82+
vfs.umount(point)
5883
except Exception:
5984
pass
6085

@@ -70,11 +95,11 @@ def _erase(bdev):
7095

7196

7297
def main():
73-
bdev, mount_point = _get_bdev()
98+
bdev = _get_bdev()
7499
if bdev is None:
75100
print("ERASE: no filesystem block device found")
76101
return
77-
_umount(mount_point)
102+
_umount(_vfs())
78103
try:
79104
count = _erase(bdev)
80105
except Exception as exc:

mpflash/mpremoteboard/format_bdev.py

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# pragma: no cover
22
"""Reformat the filesystem block device, keeping the board on MicroPython.
33
4-
Run on the board via ``mpremote run``. It locates the port's filesystem
5-
``bdev`` (the same one ``_boot.py`` mounts), detects the current filesystem
6-
type (``VfsLfs2`` or ``VfsFat``) and recreates an empty filesystem of that
7-
type, then remounts it.
4+
Run on the board via ``mpremote run``. It discovers the mounted filesystem
5+
using the same ``vfs.mount()`` enumeration as ``mpremote df`` (so the mount
6+
point and filesystem type are detected at runtime, not hardcoded per port),
7+
locates the port's block device, then recreates an empty filesystem of the
8+
same type and remounts it at the same mount point.
89
910
Only the filesystem storage region is reformatted - the firmware itself lives
1011
in a separate flash region and is left untouched.
@@ -23,50 +24,79 @@ def _vfs():
2324
return os
2425

2526

26-
def _get_bdev():
27-
"""Return (bdev, mount_point) for the running port, or (None, None)."""
28-
try:
29-
import rp2
27+
# (module, attribute, kwargs): the block-device factory each MicroPython port
28+
# exposes. Instantiated as ``module.attribute(**kwargs)``. Adding a new port is
29+
# a new entry here, not a port-name check elsewhere.
30+
_BDEV_FACTORIES = (
31+
("rp2", "Flash", {}),
32+
("samd", "Flash", {}),
33+
("nrf", "Flash", {}),
34+
("mimxrt", "Flash", {}),
35+
("alif", "Flash", {}),
36+
("pyb", "Flash", {"start": 0}), # stm32
37+
("psoc_edge", "QSPI_Flash", {}),
38+
)
3039

31-
return rp2.Flash(), "/"
32-
except Exception:
33-
pass
34-
try:
35-
import samd
3640

37-
return samd.Flash(), "/"
38-
except Exception:
39-
pass
40-
try:
41-
import nrf
41+
def _get_bdev():
42+
"""Return the internal-flash block device for the running port, or None.
4243
43-
return nrf.Flash(), "/flash"
44-
except Exception:
45-
pass
44+
Polls the known block-device factories in turn; the first that imports and
45+
instantiates wins. esp32 / esp8266 instead expose a ready-made ``bdev``.
46+
"""
47+
for mod_name, attr, kwargs in _BDEV_FACTORIES:
48+
try:
49+
return getattr(__import__(mod_name), attr)(**kwargs)
50+
except Exception:
51+
pass
4652
try:
4753
from flashbdev import bdev # esp32 / esp8266
4854

49-
return bdev, "/"
55+
return bdev
5056
except Exception:
5157
pass
58+
return None
59+
60+
61+
def _list_mounts(vfs):
62+
"""Return [(fs, mount_point), ...] via ``vfs.mount()`` (like ``mpremote df``)."""
5263
try:
53-
import pyb # stm32
64+
return list(vfs.mount())
65+
except (AttributeError, TypeError):
66+
return []
5467

55-
return pyb.Flash(start=0), "/"
56-
except Exception:
57-
pass
58-
return None, None
68+
69+
def _target_mount(vfs):
70+
"""Return (fs, mount_point) for the writable internal filesystem.
71+
72+
Uses the runtime mount table, skipping the read-only ROM filesystem and
73+
removable SD cards. Falls back to probing the usual internal-flash mount
74+
points when the mount table cannot be enumerated (older firmware).
75+
"""
76+
for fs, point in _list_mounts(vfs):
77+
if "Rom" in str(fs) or point.startswith("/sd"):
78+
continue
79+
return fs, point
80+
import os
81+
82+
for point in ("/", "/flash"):
83+
try:
84+
os.statvfs(point)
85+
return None, point
86+
except OSError:
87+
pass
88+
return None, "/"
5989

6090

6191
def _detect_fs(vfs, bdev):
62-
"""Detect the current filesystem class, defaulting to the first available.
92+
"""Probe the block device to detect its filesystem class.
6393
6494
Only Vfs* classes present in this firmware build are considered; some ports
6595
(for example nrf) are built without ``VfsFat``, so look them up defensively
6696
to avoid an ``AttributeError``.
6797
"""
6898
candidates = []
69-
for name in ("VfsLfs2", "VfsFat"):
99+
for name in ("VfsLfs2", "VfsLfs1", "VfsFat"):
70100
cls = getattr(vfs, name, None)
71101
if cls is not None:
72102
candidates.append(cls)
@@ -79,13 +109,31 @@ def _detect_fs(vfs, bdev):
79109
return candidates[0] if candidates else None
80110

81111

112+
def _fs_class(vfs, fs, bdev):
113+
"""Return the Vfs* class to recreate.
114+
115+
Prefers the type of the currently mounted filesystem (from its repr, e.g.
116+
``<VfsLfs2>``), which is the most reliable signal; if there is no mounted
117+
filesystem to learn from, probe the block device instead.
118+
"""
119+
if fs is not None:
120+
text = str(fs)
121+
for name in ("VfsLfs2", "VfsLfs1", "VfsFat"):
122+
if name in text:
123+
cls = getattr(vfs, name, None)
124+
if cls is not None:
125+
return cls
126+
return _detect_fs(vfs, bdev)
127+
128+
82129
def main():
83130
vfs = _vfs()
84-
bdev, mount_point = _get_bdev()
131+
bdev = _get_bdev()
85132
if bdev is None:
86133
print("FORMAT: no filesystem block device found")
87134
return
88-
fs_cls = _detect_fs(vfs, bdev)
135+
fs, mount_point = _target_mount(vfs)
136+
fs_cls = _fs_class(vfs, fs, bdev)
89137
if fs_cls is None:
90138
print("FORMAT: no supported filesystem type available")
91139
return
@@ -97,11 +145,11 @@ def main():
97145
try:
98146
if fs_cls is getattr(vfs, "VfsLfs2", None):
99147
fs_cls.mkfs(bdev, progsize=256)
100-
fs = fs_cls(bdev, progsize=256)
148+
new_fs = fs_cls(bdev, progsize=256)
101149
else:
102150
fs_cls.mkfs(bdev)
103-
fs = fs_cls(bdev)
104-
vfs.mount(fs, mount_point or "/")
151+
new_fs = fs_cls(bdev)
152+
vfs.mount(new_fs, mount_point or "/")
105153
except Exception as exc:
106154
print("FORMAT: failed:", exc)
107155
return

tests/flash/test_format_fs.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,68 @@ class _VfsBoth:
7171
assert detect(_VfsBoth(), object()) is _FakeFat
7272

7373

74+
def test_fs_class_prefers_mounted_fs_type():
75+
"""The mounted filesystem's own type wins over probing the block device."""
76+
77+
class _VfsBoth:
78+
VfsLfs2 = _FakeLfs2 # would succeed if probed
79+
VfsFat = _FakeFat
80+
81+
class _MountedFat:
82+
def __repr__(self):
83+
return "<VfsFat>"
84+
85+
fs_class = _load_format_bdev_namespace()["_fs_class"]
86+
assert fs_class(_VfsBoth(), _MountedFat(), object()) is _FakeFat
87+
88+
89+
def test_target_mount_skips_rom_and_sd():
90+
"""_target_mount returns the writable internal fs, skipping ROM and SD."""
91+
92+
class _Fs:
93+
def __init__(self, name):
94+
self._name = name
95+
96+
def __repr__(self):
97+
return self._name
98+
99+
rom, sd, flash = _Fs("<VfsRom>"), _Fs("<VfsFat>"), _Fs("<VfsLfs2>")
100+
101+
class _Vfs:
102+
@staticmethod
103+
def mount():
104+
return [(rom, "/rom"), (sd, "/sd"), (flash, "/flash")]
105+
106+
fs, point = _load_format_bdev_namespace()["_target_mount"](_Vfs())
107+
assert (fs, point) == (flash, "/flash")
108+
109+
110+
def test_get_bdev_polls_port_factory(monkeypatch):
111+
"""_get_bdev instantiates the first available port block-device factory."""
112+
import sys
113+
import types
114+
115+
sentinel = object()
116+
fake = types.ModuleType("mimxrt")
117+
fake.Flash = lambda: sentinel
118+
monkeypatch.setitem(sys.modules, "mimxrt", fake)
119+
120+
assert _load_format_bdev_namespace()["_get_bdev"]() is sentinel
121+
122+
123+
def test_get_bdev_falls_back_to_flashbdev(monkeypatch):
124+
"""esp32 / esp8266 expose a ready-made bdev via flashbdev."""
125+
import sys
126+
import types
127+
128+
sentinel = object()
129+
fake = types.ModuleType("flashbdev")
130+
fake.bdev = sentinel
131+
monkeypatch.setitem(sys.modules, "flashbdev", fake)
132+
133+
assert _load_format_bdev_namespace()["_get_bdev"]() is sentinel
134+
135+
74136
def _fakeboard(port="rp2", serialport="COM42"):
75137
board = MPRemoteBoard(serialport)
76138
board.connected = True

0 commit comments

Comments
 (0)