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_BLOCK_SIZE = 4096
|
||||
DEFAULT_BLOCK_COUNT = 2048
|
||||
DEFAULT_READ_SIZE = 512
|
||||
DEFAULT_LOOKAHEAD_SIZE = 256
|
||||
DEFAULT_FILTERS = [
|
||||
"highpass=f=120",
|
||||
"lowpass=f=6000",
|
||||
@@ -26,6 +28,7 @@ DEFAULT_SYS_PROMPTS = [
|
||||
{"id": "404", "text": "No sound sample was found on the device."},
|
||||
{"id": "update", "text": "Firmware updated. Awaiting confirmation."},
|
||||
{"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)
|
||||
|
||||
|
||||
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(
|
||||
block_size=block_size,
|
||||
block_count=block_count,
|
||||
read_size=256,
|
||||
read_size=DEFAULT_READ_SIZE,
|
||||
prog_size=256,
|
||||
lookahead_size=512,
|
||||
lookahead_size=DEFAULT_LOOKAHEAD_SIZE,
|
||||
cache_size=4096,
|
||||
)
|
||||
|
||||
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()
|
||||
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.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:
|
||||
@@ -203,7 +256,7 @@ def main() -> None:
|
||||
tts_count = build_sys_tts(prompts, out_sys, 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,
|
||||
output_hex=output_hex,
|
||||
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("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():
|
||||
shutil.rmtree(staging_root)
|
||||
|
||||
Reference in New Issue
Block a user