Update filesystem tooling and LittleFS defaults
This commit is contained in:
86
firmware/fs/README.md
Normal file
86
firmware/fs/README.md
Normal 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.
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user