Skip to content

FileSystem

Rob Dobson edited this page May 3, 2026 · 1 revision

File System

Raft mounts a flash-resident file system (LittleFS or SPIFFS) and, optionally, an SD card. The same FileSystem object is used by every SysMod, by the WebServer for static-asset delivery, by the DataLogger for autonomous CSV/JSONL capture and by FileManager to expose REST endpoints. This page covers how data gets into a build, how it is mounted at runtime, and which API to call from C++.

The code lives at components/core/FileSystem/ in RaftCore. The single global FileSystem fileSystem; instance is created by RaftCoreApp and configured by the FileManager SysMod using values from your SysType JSON.

Where the file system lives

A Raft device exposes up to two file systems, identified by name:

FS name Mount point Backing storage Notes
local /local LittleFS or SPIFFS in the fs partition Always present unless explicitly disabled. Alias spiffs accepted for legacy callers.
sd /sd FAT-on-SPI SD card Optional; requires RAFT_ENABLE_SD and pin configuration.

When a REST API or C++ call passes an empty file-system name, getDefaultFSRoot() picks one. The default is local, but if DefaultSD is set in FileManager configuration and an SD card is present at boot, default operations resolve to sd instead.

The local partition is always labelled fs (with spiffs as a fallback label for older partition tables) — see Partitions and Flash Layout for the layout details.

LittleFS vs SPIFFS

Raft supports both, selected at build time by the SysType's FS_TYPE value (in features.cmake):

set(FS_TYPE "littlefs")     # or "spiffs"

This single value drives three things in the build:

  1. Which IDF component is linked. RaftCore/CMakeLists.txt requires littlefs (and defines FILE_SYSTEM_SUPPORTS_LITTLEFS) for littlefs, or spiffs (and defines RAFT_FILESYSTEM_HAS_SPIFFS) otherwise.
  2. Which image generator is used. RaftGenFSImage.cmake calls littlefs_create_partition_image(fs … FLASH_IN_PROJECT) or spiffs_create_partition_image(fs … FLASH_IN_PROJECT) accordingly.
  3. Which subtype your partitions.csv must declare for the fs partition: 0x83 for LittleFS, spiffs for SPIFFS. Mismatches cause mount failure on first boot — and a re-format if LocalFsFormatIfCorrupt is set.

Recommendation: prefer LittleFS for new projects. It tolerates power loss, supports nested directories and is the default in current Raft examples. Keep SPIFFS only for legacy partition layouts that cannot be reformatted in the field.

At runtime, FileSystem::localFileSystemSetup() actually probes both — if LittleFS support is compiled in, it tries to mount LittleFS first regardless of LocalFsDefault, and only falls back to SPIFFS if the partition does not look like LittleFS. This means a device that originally shipped with SPIFFS can be migrated to LittleFS by switching FS_TYPE and reflashing.

Including files in the build (FS image)

Anything you want pre-loaded into local at first boot goes into a file-system image that is built and flashed alongside the firmware.

The Raft pipeline accepts content from two places, merged together by CopyFSAndWebUI:

+--------------------------+      +------------------------------+
| FS_IMAGE_PATH (your data |      | UI_SOURCE_PATH (Web UI src)  |
| files: configs, scripts, |      | parcel/npm bundle output     |
| audio, calibration .json |      | gzipped .html/.js/.css       |
| …)                       |      |                              |
+------------+-------------+      +--------------+---------------+
             \\                                  /
              \\        CopyFSAndWebUI         /
               \\______________________________/
                          |
                          v
            build/<sys>/raft/FSImage/
                          |
                          v
       littlefs_create_partition_image(fs FSImage/ FLASH_IN_PROJECT)
                          |
                          v
                    fs.bin (flashed with the rest of the project)

Adding a data folder

In your SysType's features.cmake:

set(FS_TYPE         "littlefs")
set(FS_IMAGE_PATH   "../Common/FSImage")        # source folder, relative to SysType
set(UI_SOURCE_PATH  "../Common/WebUI")          # optional; see WebUIBuildPipeline

Anything under systypes/Common/FSImage/ (or whatever path you set) is copied verbatim into the file-system image. Typical contents:

systypes/Common/FSImage/
├── config/
│   └── default-pipeline.json     ← read by a SysMod at boot
├── certs/
│   └── ca.pem                    ← MQTT TLS root cert
└── tunes/
    ├── startup.wav
    └── alarm.wav

After flashing, those appear at /local/config/default-pipeline.json, /local/certs/ca.pem, etc.

Skipping the FS image

If a SysType has no Web UI and no data files, leave FS_IMAGE_PATH unset. RaftGenFSImage.cmake is still useful (the partition is created empty) but the fs.bin will be tiny.

To skip image creation entirely, drop the include(${raftcore_SOURCE_DIR}/scripts/RaftGenFSImage.cmake) line from your project's top-level CMakeLists.txt — the partition will be left blank on flash, and the device will format it on first boot if LocalFsFormatIfCorrupt is set.

Sizing

The FS image must fit the fs partition or the build fails. Typical sizes:

  • 256 KB — a few configs, no Web UI.
  • 512 KB — the parcel-built React/MUI starter Web UI.
  • 1–4 MB — Web UI plus audio/image assets.

See Partitions and Flash Layout to grow the partition.

Adding an SD card

Set RAFT_ENABLE_SD (a project-level CMake variable, often added to your top-level CMakeLists.txt or features.cmake) to pull in the fatfs and sdmmc ESP-IDF components and define RAFT_FILESYSTEM_HAS_FATSD:

set(RAFT_ENABLE_SD ON CACHE BOOL "" FORCE)

Then enable and configure SD pins in your SysType's SysTypes.json:

{
    "name": "FileManager",
    "LocalFsDefault": "littlefs",
    "SDEnabled": 1,
    "SDMOSI": "23",
    "SDMISO": "19",
    "SDCLK":  "18",
    "SDCS":   "5",
    "DefaultSD": 0,
    "CacheFileSysInfo": 1
}

Pin values can be GPIO numbers ("5") or symbolic pin names resolved by ConfigPinMap (e.g. "SD_CS"). With SDEnabled set the FileManager calls FileSystem::setup(...) with SD parameters; the FileSystem then mounts FAT on the card and exposes it as /sd. Set DefaultSD to make file ops with no FS-name argument resolve to the SD card.

Mounting at runtime

The FileSystem mount sequence happens during the FileManager SysMod's setup(). The sequence is:

  1. FileSystem::setup(localFsType, formatIfCorrupt, enableSD, …) is called with SysType-derived parameters.
  2. Local FS is probed:
    • LittleFS first (if compiled in). On success, _localFsType = LOCAL_FS_LITTLEFS and the partition is mounted at /local.
    • SPIFFS next (if compiled in and LittleFS failed).
    • If both fail, the FS is formatted (if LocalFsFormatIfCorrupt is true) and re-tried.
    • If everything fails, _localFsType = LOCAL_FS_DISABLE and local becomes unavailable.
  3. SD is initialised if SDEnabled. Mount failure is logged but never fatal.
  4. The cache is primed (10 service iterations) so the first filelist call returns immediately.

The cache (enabled with CacheFileSysInfo) is critical for serving Web UI listings on resource-constrained boards — getFilesJSON() uses it directly when the request is for the FS root.

C++ API

All APIs go through the global fileSystem object. The most useful entry points:

#include "FileSystem.h"

// Pass "" as fileSystemStr to use the default FS

// One-shot read (returns a heap-allocated, NUL-terminated buffer; caller frees)
uint8_t* pBuf = fileSystem.getFileContents("", "config/default.json");
if (pBuf) {
    // … use as char* …
    free(pBuf);
}

// One-shot write
String contents = "key=value";
fileSystem.setFileContents("local", "settings.txt", contents);

// Existence and metadata
uint32_t size = 0;
if (fileSystem.getFileInfo("", "tunes/startup.wav", size)) { … }

// Streamed I/O — preferred for anything that might be large
FILE* pFile = fileSystem.fileOpen("", "log.csv", /*write=*/true, /*seekToPos=*/0, /*seekFromEnd=*/true);
if (pFile) {
    fileSystem.fileWrite(pFile, (const uint8_t*)line, lineLen);
    fileSystem.fileClose(pFile, "", "log.csv", /*fileModified=*/true);
}

// Reformat (destructive)
String resp;
fileSystem.reformat("local", resp, /*force=*/false);

Use FileSystemChunker when reading or writing in fixed-size blocks — the OTA path, the file-download protocol and the DataLogger all use it. The chunker keeps the FILE* open across calls and serialises mutex acquisition on its own.

A few invariants worth knowing:

  • Single mutex. FileSystem serialises every operation with _fileSysMutex. Long reads or writes block other callers, so chunk large operations rather than holding the file open across many loop() iterations.
  • getFileContents allocates from PSRAM if available. It returns nullptr if the file is larger than ~⅓ of the largest available allocation block. For files larger than that, use the streamed API or a FileSystemChunker.
  • Filenames may include the FS name. "/local/foo.txt", "local/foo.txt" and "foo.txt" (with fileSystemStr="local") all resolve to the same path.

REST endpoints

The FileManager SysMod exposes a thin wrapper for HTTP/RICREST clients. See Built-in REST Endpoints for the full list — filelist, fileread, filedelete, fileupload, reformatfs. Folder paths use ~ instead of / so they survive URL parsing.

Troubleshooting

Symptom Likely cause Fix
localFileSystemSetup no file system found at boot Partition subtype mismatch (spiffs vs 0x83) or partition table not flashed Match FS_TYPE to the fs partition subtype, then idf.py erase-flash flash
LittleFS image creation failed: too large FSImage/ exceeds the fs partition size Grow fs in partitions.csv or trim assets
getContents … too big to read File larger than the in-memory threshold Use fileOpen/fileRead or FileSystemChunker instead
First boot wipes the FS image Mount failed and LocalFsFormatIfCorrupt reformatted Verify partition subtype; flash the FS image with idf.py flash (not just app-flash)
unknownfs sd from filelist SD not enabled or pins wrong Enable RAFT_ENABLE_SD, set SDEnabled and SPI pins, check card
fileupload succeeds but file missing Cache returned stale listing Either set CacheFileSysInfo: 0 while debugging, or re-fetch — uploads call markFileCacheDirty automatically

See also

Clone this wiki locally