Storage & Persistent Data

Two ways to keep data on the board, and one guarantee that covers both: values live in the board’s storage partition, not the application image, so they survive re-flashing the app. (Erasing the whole flash wipes them.)

  • Store — typed key/value settings for device state: counters, calibration, flags. One namespace per store.
  • File — whole-file text I/O on littlefs for blobs a person might read or edit: notes, exported JSON, a boot log.

There is no session on either one. No begin(), no mount(), no open()/close(). littlefs mounts lazily on first use; the settings backend loads once at boot.


Store — key/value settings

Construct a store with a namespace; every read and write rides that namespace ('app' becomes the tc/app/ settings subtree), so several stores can coexist without colliding.

import { Store } from '@typecad/hal';
import { UART0 } from '@typecad/board';

const settings = new Store('app');

const boots = settings.getInt('boots', 1);   // first boot reads the default: 1
if (boots === 1) {
  UART0.writeLine('first run');
}
settings.setInt('boots', boots + 1);

settings.setString('ssid', 'MyNet');
settings.setFloat('offset', -1.25);
settings.setBool('calibrated', true);

UART0.writeLine(settings.getString('ssid', '(none)'));

The get methods take a required default. There is no hidden zero or empty-string value for “absent” — an unset key reads back exactly the default you chose, which makes first-boot logic explicit.

settings.remove('ssid');   // delete one key
settings.clear();          // delete every tc/app/* key this program wrote

File — whole-file text I/O

Construct with the path; the four verbs map onto littlefs one-for-one.

import { File } from '@typecad/hal';

const log = new File('/log.txt');

log.write('boot\n');              // overwrite (creates the file when absent)
if (log.exists()) {
  UART0.writeLine(log.read());        // 'boot\n'
  log.remove();
}

Things worth knowing:

  • read() returns "" for a missing file and an empty file alike — check exists() first when the difference matters.
  • write() overwrites; there is no append. To append, read, concatenate, write back.
  • write() creates the file when absent, but not parent directories — keep files at the root of the partition.
  • Return buffers are shim-owned until the next read — copy anything you need to keep.

A boot counter that survives re-flashing is two lines:

const boot = new File('/boot-count.txt');
boot.write(String(Number(boot.read() || '0') + 1));

Which one to use

StoreFile
ShapeMany typed keys under a namespaceOne whole text document per path
BackendZephyr settings / ZMSlittlefs
Best atCounters, calibration, flagsNotes, exported JSON, logs a person reads
WritesPer-key commitsWhole-file rewrites
TypesTyped pairs, required defaultsNone — it’s text
⚙️ Advanced details — where the data physically lives

Both backends live in the board’s storage_partition (internal flash). If the board’s devicetree ships no storage partition — most STM32 boards define only bootloader slots — Cuttlefish generates one near the top of flash when your program uses Store or File. Anything in flash is readable by anyone holding the board; neither store is a place for secrets.