Bedtime. /Persistence

Persistence

Saving and loading settings, and the .proto build path

There is no settings API - persistence is the Storage API calls plus a convention. This page is that convention: when to save, where to put the file, and how to lay out its bytes so that shipping a new version of a tapp does not orphan settings saved by earlier versions.

A complete working app is at examples/persistence/.

When to save

The memory_if hooks on the descriptor are optional: load() runs after the tapp's init() at launch, save() when the device flushes its settings and when the tape is swapped while the tapp owns the engine - never on close. The dependable contract is therefore two calls made by the tapp itself:

deinit() runs on a normal exit. It does not run if the battery dies, so anything the user would not want to lose should also be written at the moment it changes - or behind an explicit "save" button, which is what the example does.

Warning — Never touch storage from process(). Every call here blocks on the SD card for milliseconds; the audio callback has microseconds. Use init(), deinit(), tick() or an input handler.

Where to put the file

Any path may be used - each tapp has a private folder and every write lands in it. The firmware rewrites the path to .data/<appname>/<path> on the card, where <appname> comes from the manifest name. Writes cannot reach outside it:

#define MY_FILE "/settings.pb"   // -> .data/myapp/settings.pb on the card

storage_write_file(MY_FILE, buf, n);

.data/ is hidden but sits at the card root, so a user can still copy a save off over USB.

Reading is not restricted the same way: a read falls back to the card when the file is not in the tapp's folder, which is what keeps browser_open results and foreign assets (ROMs, samples, WAVs) loadable. See Reads.

The quick way: a fixed struct

For a handful of values that will never change shape, write the struct. Guard it with a magic word and check the length, because the file lives on a removable card that a host can overwrite with anything:

#define HISCORE_MAGIC 0x54544831u

static void load_hiscore(my_model_t* m) {
    uint32_t rec[2];
    if(storage_read_file("/hiscore.bin", rec, sizeof(rec)) != sizeof(rec)) return;
    if(rec[0] != HISCORE_MAGIC) return;
    m->hiscore = rec[1];
}

static void save_hiscore(const my_model_t* m) {
    const uint32_t rec[2] = {HISCORE_MAGIC, m->hiscore};
    storage_write_file("/hiscore.bin", rec, sizeof(rec));
}

The limitation is that adding a field invalidates every file already written. Bumping the magic discards old saves; omitting the bump makes them misread. That is acceptable for a high score and costly for a settings page.

Note — Test the write result as > 0, never == size. On device storage_write_file() returns the byte count.

The durable way: a .proto schema

Protobuf stores field numbers rather than offsets, so a reader skips fields it does not know and defaults the ones that are absent. A field can be added in an update and files written by earlier versions still load. The cost is roughly 4KB of nanopb runtime in the tapp.

Place a .proto next to the tapp's .c; tapp-build handles the rest:

// myapp/settings.proto
syntax = "proto3";
import "nanopb.proto";

message PB_Settings {
    uint32 ver = 1;
    uint32 bpm = 2;
    string label = 3 [(nanopb).max_size = 16];
}

Build the folder, not the file - ./tapp-build myapp. The generated settings.pb.h and settings.pb.c land in a temporary directory that is already on the include path, and the nanopb runtime is compiled and linked in automatically:

#include "tapp_api.h"
#include <pb_encode.h>
#include <pb_decode.h>
#include "settings.pb.h"

static bool settings_save(const my_model_t* m) {
    PB_Settings pb = PB_Settings_init_zero;
    pb.ver = 1;
    pb.bpm = m->bpm;
    strncpy(pb.label, m->label, sizeof(pb.label) - 1);

    uint8_t buf[PB_Settings_size];              // generated: the largest possible encoding
    pb_ostream_t out = pb_ostream_from_buffer(buf, sizeof(buf));
    if(!pb_encode(&out, PB_Settings_fields, &pb)) return false;

    return storage_write_file("/settings.pb", buf, out.bytes_written) > 0;
}

Notes before adopting it:

Version it, then distrust it

Make the first field a version number and refuse anything that disagrees. proto3 cannot distinguish an absent field from a zero one, so this is the only cheap way to reject a file written when field 4 meant something else.

A successful decode says the bytes were well-formed, not that the values are usable. Clamp numbers to the range the code assumes and treat every index or enum as hostile - this file is user-writable over USB, and an out-of-range index read straight into an array subscript is a hard fault:

if(pb.ver != SETTINGS_VER) return false;                         // reject, use defaults
m->bpm = pb.bpm < 40u ? 40u : (pb.bpm > 300u ? 300u : pb.bpm);   // clamp, never trust