Update filesystem tooling and LittleFS defaults

This commit is contained in:
2026-05-11 14:01:56 +02:00
parent 12e82d35d2
commit e85d51488e
3 changed files with 153 additions and 6 deletions

86
firmware/fs/README.md Normal file
View File

@@ -0,0 +1,86 @@
# FS Tooling
This directory contains the tooling to build and flash the external LittleFS image used by the firmware.
## Python Version
Use Python 3.11.
The filesystem/audio build tool is currently tested with Python 3.11.
Python 3.13 is not supported at the moment, especially on Windows, because some dependencies in the TTS toolchain may fall back to source builds and fail to install.
In particular, the dependency chain around `kokoro`, `spacy`, `thinc`, and `blis` may fail to install on Windows when no compatible wheels are available.
## Install Dependencies
`ffmpeg` is required in addition to the Python packages.
```sh
python3.11 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
```
On Windows:
```bat
py -3.11 -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
```
## Build LittleFS Image
```sh
./venv/bin/python build_lfs_audio.py
```
Example with imported WAV files and a custom system prompt YAML:
```sh
./venv/bin/python build_lfs_audio.py \
--wav-dir ./my_wavs \
--sys-yaml ./sys_prompts.yaml
```
Notes:
- `--wav-dir` imports all `.wav` files from the given directory into `/lfs/a`
- `--sys-yaml` overrides the default TTS prompt list for `/lfs/sys`
- generated audio is converted to mono 16 kHz PCM before it is written into the LittleFS image
This generates:
- `lfs_external_flash.hex`
The default image layout is aligned to a 4 KiB flash page layout.
After the build, the script also prints a usage summary for `/lfs/sys`, `/lfs/a`, and the combined image.
Example:
```text
Usage summary:
sys: files=4, raw=48.0 KiB, fs=56.0 KiB (14 blocks, standalone estimate)
audio: files=12, raw=224.0 KiB, fs=264.0 KiB (66 blocks, standalone estimate)
total: raw=272.0 KiB, fs=300.0 KiB (75 blocks in combined image)
```
`raw` is the sum of file sizes in the staging tree.
`fs` is the estimated or measured LittleFS space usage with the configured block size.
## Flash Image
On macOS/Linux:
```sh
./program.sh
```
On Windows:
```bat
program.bat
```
Both scripts use paths relative to their own location, so they can be started from any working directory.

View File

@@ -15,6 +15,8 @@ DEFAULT_SAMPLE_RATE = 16000
DEFAULT_VOICE = "af_bella" DEFAULT_VOICE = "af_bella"
DEFAULT_BLOCK_SIZE = 4096 DEFAULT_BLOCK_SIZE = 4096
DEFAULT_BLOCK_COUNT = 2048 DEFAULT_BLOCK_COUNT = 2048
DEFAULT_READ_SIZE = 512
DEFAULT_LOOKAHEAD_SIZE = 256
DEFAULT_FILTERS = [ DEFAULT_FILTERS = [
"highpass=f=120", "highpass=f=120",
"lowpass=f=6000", "lowpass=f=6000",
@@ -26,6 +28,7 @@ DEFAULT_SYS_PROMPTS = [
{"id": "404", "text": "No sound sample was found on the device."}, {"id": "404", "text": "No sound sample was found on the device."},
{"id": "update", "text": "Firmware updated. Awaiting confirmation."}, {"id": "update", "text": "Firmware updated. Awaiting confirmation."},
{"id": "confirm", "text": "State confirmed."}, {"id": "confirm", "text": "State confirmed."},
{"id": "voltest", "text": "Volume test. This is a sample of the current volume level."},
] ]
@@ -129,18 +132,32 @@ def add_files_recursive(fs: LittleFS, local_path: Path, lfs_path: str = "/") ->
add_files_recursive(fs, item, target_path) add_files_recursive(fs, item, target_path)
def build_littlefs_hex(source_folder: Path, output_hex: Path, block_size: int, block_count: int, start_addr: int) -> None: def build_littlefs_image(source_folder: Path, block_size: int, block_count: int) -> tuple[LittleFS, int]:
fs = LittleFS( fs = LittleFS(
block_size=block_size, block_size=block_size,
block_count=block_count, block_count=block_count,
read_size=256, read_size=DEFAULT_READ_SIZE,
prog_size=256, prog_size=256,
lookahead_size=512, lookahead_size=DEFAULT_LOOKAHEAD_SIZE,
cache_size=4096, cache_size=4096,
) )
add_files_recursive(fs, source_folder) add_files_recursive(fs, source_folder)
used_blocks = 0
lfs_buffer = fs.context.buffer
for idx in range(block_count):
offset = idx * block_size
block_data = lfs_buffer[offset : offset + block_size]
if any(byte != 0xFF for byte in block_data):
used_blocks += 1
return fs, used_blocks
def build_littlefs_hex(source_folder: Path, output_hex: Path, block_size: int, block_count: int, start_addr: int) -> int:
fs, used_blocks = build_littlefs_image(source_folder, block_size, block_count)
ih = IntelHex() ih = IntelHex()
lfs_buffer = fs.context.buffer lfs_buffer = fs.context.buffer
@@ -151,6 +168,42 @@ def build_littlefs_hex(source_folder: Path, output_hex: Path, block_size: int, b
ih.frombytes(block_data, offset=start_addr + offset) ih.frombytes(block_data, offset=start_addr + offset)
ih.tofile(str(output_hex), format="hex") ih.tofile(str(output_hex), format="hex")
return used_blocks
def summarize_directory(directory: Path) -> tuple[int, int]:
if not directory.exists():
return 0, 0
file_count = 0
raw_bytes = 0
for file_path in directory.rglob("*"):
if file_path.is_file():
file_count += 1
raw_bytes += file_path.stat().st_size
return file_count, raw_bytes
def format_bytes(size: int) -> str:
units = ["B", "KiB", "MiB", "GiB"]
value = float(size)
for unit in units:
if value < 1024.0 or unit == units[-1]:
if unit == "B":
return f"{int(value)} {unit}"
return f"{value:.1f} {unit}"
value /= 1024.0
def report_directory_usage(label: str, directory: Path, block_size: int, block_count: int) -> None:
file_count, raw_bytes = summarize_directory(directory)
_, used_blocks = build_littlefs_image(directory, block_size, block_count)
fs_bytes = used_blocks * block_size
print(
f" {label}: files={file_count}, raw={format_bytes(raw_bytes)}, "
f"fs={format_bytes(fs_bytes)} ({used_blocks} blocks, standalone estimate)"
)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
@@ -203,7 +256,7 @@ def main() -> None:
tts_count = build_sys_tts(prompts, out_sys, args.sample_rate) tts_count = build_sys_tts(prompts, out_sys, args.sample_rate)
wav_count = import_wavs_to_a(wav_dir, out_a, args.sample_rate) wav_count = import_wavs_to_a(wav_dir, out_a, args.sample_rate)
build_littlefs_hex( total_used_blocks = build_littlefs_hex(
source_folder=staging_root, source_folder=staging_root,
output_hex=output_hex, output_hex=output_hex,
block_size=args.block_size, block_size=args.block_size,
@@ -212,6 +265,14 @@ def main() -> None:
) )
print(f"Done. TTS assets: {tts_count}, WAV imports: {wav_count}, HEX: {output_hex}") print(f"Done. TTS assets: {tts_count}, WAV imports: {wav_count}, HEX: {output_hex}")
print("Usage summary:")
report_directory_usage("sys", out_sys, args.block_size, args.block_count)
report_directory_usage("audio", out_a, args.block_size, args.block_count)
print(
f" total: raw={format_bytes(summarize_directory(staging_root)[1])}, "
f"fs={format_bytes(total_used_blocks * args.block_size)} "
f"({total_used_blocks} blocks in combined image)"
)
if not args.keep_staging and staging_root.exists(): if not args.keep_staging and staging_root.exists():
shutil.rmtree(staging_root) shutil.rmtree(staging_root)

View File

@@ -58,13 +58,13 @@ if FS_MGMT
endif # SOC_SERIES_NRF52X endif # SOC_SERIES_NRF52X
config FS_LITTLEFS_READ_SIZE config FS_LITTLEFS_READ_SIZE
default 256 default 512
config FS_LITTLEFS_PROG_SIZE config FS_LITTLEFS_PROG_SIZE
default 256 default 256
config FS_LITTLEFS_CACHE_SIZE config FS_LITTLEFS_CACHE_SIZE
default 4096 default 4096
config FS_LITTLEFS_LOOKAHEAD_SIZE config FS_LITTLEFS_LOOKAHEAD_SIZE
default 512 default 256
module = FS_MGMT module = FS_MGMT
module-str = fs_mgmt module-str = fs_mgmt