SDK

AO_Storage

The Active Object that owns the microSD card and the internal FAT: the only code that opens a file, with loaned reads and paged listings.

What it is

A Kode Dot keeps files in two places: the microSD card and the internal FAT partition. AO_Storage owns both and is the only code in the SDK that opens a file. Everything else sends it a request and gets events back: a list of files, a chunk of a file, a write, a copy, a delete. A request names a file on a medium, or gives a full path that starts with /, /sdcard/… or /storage/….

It reads a file in 32 KB chunks through two buffers in PSRAM. It lends one buffer to the reader, who gives it back when the bytes are used, so the next chunk is read while the first is in use. The system allows one reader and one writer at a time, and cp is the one case that needs both. AO_Storage also runs the card tests of the production line and the bench.

How it is connected

flowchart LR
subgraph own["AO_Storage and below"]
  sto["AO_Storage<br/>priority 5"] -- "job" --> w["storage_sd<br/>worker"]
  w -- "mount, sectors,<br/>format" --> sd["kdrv_sd"]
  sto -- "files" --> st["kdrv_storage"]
end
appmgr["AO_AppMgr"] -- "open, read, next" --> sto
con["AO_Console"] -- "path, pair, text" --> sto
gui["AO_Gui, AO_Audio<br/>facade"] -- "list, open, write" --> sto
sd --> card["microSD"]
st --> fat["Internal FAT"]
class sto,appmgr,con,gui ao
class card,fat store
Who asks AO_Storage, and what it calls

Priority and the defer queue

AO_Storage runs at priority 5 with a queue of 16 events and a 6144-byte stack. It claims KBSP_RES_SDMMC and KBSP_RES_FATFS. The priority is low on purpose: a slow card is the longest wait in the system, and down here it can delay only AO_Net, AO_Gui and AO_Radio. Its steps stay short. A write moves 32 KB per step and a timer gives lower priorities one slot between steps.

The AO has one state, Storage_active, with a filter in front of it. While a call to the card is out, any request that could touch the card waits in a defer queue of 8 (KODE_STORAGE_DEFER_DEPTH) and is taken again in order when the call returns. A request over that room is refused with an answer, never in silence. A request for the internal partition is served at once.

The worker

storage_sd is a task of AO_Storage with an 8192-byte stack in PSRAM and a queue of one job (sdk/kqp). It runs one call of kdrv_sd at a time and decides nothing: mount, unmount, lend, return, read or write raw sectors, format. AO_Storage reads the status in STORAGE_SDJOB_DONE_SIG.

Every call has a deadline, the driver's bound plus KODE_STORAGE_JOB_MARGIN_MS (500 ms). When it fires, AO_Storage tells the waiter the call failed, and the late result answers nobody twice. An overrun in the middle of a card script ends the script, because the next phase would ask the card for a second call while the worker is still inside the first.

The stack is in PSRAM because nothing a job touches reaches the internal flash. The test file of the card row can land on the internal FAT, so its write stays in the AO.

One question, busy()

Six facts say whether a file handle is in use:

Almost every path asks busy() first and refuses while it is true.

Listing

storage_list_request() posts an unmount of the card, a mount and the first page of bootable images, in that order. The remount makes the listing honest: FATFS keeps what it read at mount, so a swapped card would stay unseen otherwise. The internal partition is not remounted.

storage_list_page(skip, who) walks the card first and the internal FAT second as one listing, and posts up to 24 STORAGE_ENTRY_SIG events and an end marker to the asker alone. pos counts across pages and media, and more on the end marker says another page follows. A name of 64 characters or more is skipped.

The end marker comes from two static events, so an empty pool cannot lose it. sd_ls and fs_ls publish one page of 24 and say capped.

Reading, the loan

The reader sends STORAGE_OPEN_REQ_SIG and gets STORAGE_OPEN_RSP_SIG with the size. A bare name is searched on the card first and then on the internal FAT, and a name starting with / opens exactly that file.

STORAGE_READ_REQ_SIG makes AO_Storage fill a free buffer and post STORAGE_DATA_SIG (idx, len, eof). The bytes are valid until the reader returns the index with STORAGE_NEXT_SIG, which also asks for the next chunk. Only the index travels, never an address.

The manifest peek and the app data folder

STORAGE_DESC_REQ_SIG reads the front of a .bin and answers its project name, display name and category, inside one step and without marking the asker as the reader.

APPDATA_REQ_SIG resolves the folder /storage/.kode/appsdata/<project>, tries the internal FAT before the card and uses the first folder it finds whole. It posts one APPDATA_FIELDS_SIG, zero or three APPDATA_ICON_SIG and an APPDATA_END_SIG. The conversation always closes, so a launcher scan never stalls.

APPDATA_RM_REQ_SIG deletes the folder, one level of subfolder included, on the internal FAT only. It never touches the card.

The path requests

The console and the facade never open a file themselves. STORAGE_PATH_REQ_SIG carries STAT, LIST, CAT, MKDIR, UNLINK and RMDIR. STORAGE_PAIR_REQ_SIG carries MOVE and COPY. A move across /sdcard and /storage is refused with KODE_DENIED, since FAT cannot rename across a mount.

STORAGE_TEXT_REQ_SIG writes text, or replaces one line by writing a .tmp file, renaming the original to .bak and the new file into place.

CAT sends 24 lines per step, 200 bytes at most per line. COPY borrows one buffer, copies 32 KB per step and removes the destination whole if it fails.

Writing

storage_write_all_request() takes a payload that is already whole in the caller's memory, which must stay valid until STORAGE_WRITE_RSP_SIG. A name with no path goes to the internal FAT and falls over to the card. After a failed chunk the stub is discarded and the write retries once on the other medium. A failed write never leaves a partial file.

Receiving a file

file_put uses the five receive signals that AO_AppMgr uses (sdk/kaos/appmgr), but AO_Storage posts them and does not publish them, so the Opening card never appears. It arms raw mode on the console first, and opens the destination only after the port is raw. The idle timer starts as a 30 s long-stop and tightens to 5 s before RECV_READY_SIG goes out.

Lending the card

kode_sd_lend() makes AO_Storage unmount the volume and keep the card for raw sectors. While it is lent every /sdcard path answers as a volume that does not mount, every mount is refused and neither card script starts. kode_sd_return() takes it back and mounts again.

The card tests

test component=sd judges the board's slot and leaves the card as it was. It has five phases and publishes one STORAGE_TEST_SIG for each: identify, read4, read1, write, verify.

Only identify and read4 decide the verdict, since counterfeit cards read well and refuse writes.

sd_verify judges the card itself, destructively, with probe and format. Both end in a published TEST_RESULT_SIG.

Resting with the board

With CONFIG_KODE_LOWPOWER, AO_Storage joins the rounds of AO_LowPower as storage (sdk/kaos/lowpower). A file is open only while a transfer runs, so the rest is a check. With nothing open, no call out to the card and no request held, the files are closed and the card is idle, and the round is answered ready. An open reader or writer answers file write. Any other transfer, or a call to the card that is out, answers storage busy.

The card's supply is not AO_Storage's: deep sleep holds it off. Nothing is restored when the board wakes, so AO_Storage listens to the rounds and not to LOWPOWER_STATE_SIG.

Specifications

Requests

SignalEventWhat AO_Storage does
STORAGE_CMD_SIGStorageCmdEvt (medium, op, images_only, last, who)op is STORAGE_OP_MOUNT, _UNMOUNT, _INFO, _LIST, _SELFTEST or _VERIFY. With who set it posts STORAGE_CMD_RSP_SIG when the command has run
STORAGE_LIST_PAGE_SIGStorageListPageEvt (skip, who)One page of bootable images, to who alone
STORAGE_OPEN_REQ_SIG, STORAGE_READ_REQ_SIG, STORAGE_NEXT_SIG, STORAGE_CLOSE_REQ_SIGStorageOpenEvt, none, StorageBufEvt, noneThe loan. Close ends a read only, and is ignored while a write, copy or file_put runs
STORAGE_WRITE_REQ_SIGStorageWriteEvtPut a caller's data in a file
STORAGE_DESC_REQ_SIGStorageDescReqEvtThe manifest peek
APPDATA_REQ_SIG, APPDATA_RM_REQ_SIGAppDataReqEvt (project, who)Read or delete an app's data folder
STORAGE_PATH_REQ_SIG, STORAGE_PAIR_REQ_SIG, STORAGE_TEXT_REQ_SIGStoragePathEvt, StoragePairEvt, StorageTextEvtThe file tree
STORAGE_FIELD_REQ_SIGStorageFieldReqEvt (op, field, value, who)Get, set or dump a field of the card's table. A set is followed by a get
STORAGE_LEND_REQ_SIGStorageLendEvt (lend, wait, gen)Lend or take back the card. Refused with KODE_DENIED while a script runs or busy() is true
STORAGE_RECV_REQ_SIGStorageRecvEvtStart a file_put
CONSOLE_RAW_RSP_SIGConsoleRawRspEvtAO_Log's answer to the raw-mode request
RECV_DATA_SIGRecvDataEvtA chunk of a file_put, from the reader task or the NimBLE task
LOWPOWER_ROUND_SIGLowPowerRoundEvtWith CONFIG_KODE_LOWPOWER: a round of the board's rest, answered ready or busy

Private signals: STORAGE_SDJOB_DONE_SIG (a KqpWorkerDoneEvt from storage_sd), and the QTimeEvts STORAGE_SDJOB_TO_SIG, STORAGE_WRITE_NEXT_SIG, STORAGE_TEST_NEXT_SIG, STORAGE_CAT_NEXT_SIG, STORAGE_COPY_NEXT_SIG and STORAGE_RECV_IDLE_SIG.

Answers

SignalEventRangeTo
STORAGE_ENTRY_SIGStorageEntryEvt (name, size, from_sd, pos, is_dir)publish for sd_ls, fs_ls, ls; post for a pageSubscribers; the asker of a page
STORAGE_LIST_END_SIGListEndEvt (more)sameSame
STORAGE_INFO_SIGStorageInfoEvt (medium, mounted, usage_known, total_kb, free_kb, card_mb)publishCloses sd_info and fs_info. card_mb is the size of the card, and the volume on it can be smaller
STORAGE_TEST_SIG, TEST_RESULT_SIGStorageTestEvt, TestResultEvtpublishOne per phase of a card script, then the verdict
STORAGE_OPEN_RSP_SIG, STORAGE_DATA_SIG, STORAGE_WRITE_RSP_SIG, STORAGE_DESC_RSP_SIG, STORAGE_FIELD_RSP_SIGMatching eventspostwho
STORAGE_PATH_RSP_SIG, STORAGE_LINE_SIG, STORAGE_CMD_RSP_SIGStoragePathRspEvt, StorageLineEvt, nonepostwho
APPDATA_FIELDS_SIG, APPDATA_ICON_SIG, APPDATA_END_SIG, APPDATA_RM_RSP_SIGApp data eventspostwho
RECV_READY_SIG, RECV_ACK_SIG, RECV_DONE_SIG, RECV_FAIL_SIG, RECV_STALL_SIGnonepostThe who of the file_put
CONSOLE_RAW_REQ_SIG, CONSOLE_RAW_END_SIGConsoleRawReqEvt, nonepostAO_Log

Limits and timers

NameValueMeaning
STORAGE_BUFS, STORAGE_CHUNK2, 32 KBStreaming buffers in PSRAM, aligned to 64 bytes
KODE_STORAGE_LIST_MAX24Entries in a page; a page and its end marker are 25 events
KODE_APP_FILE_MAX64Longest listed file name
KODE_PATH_MAX96Longest path
KODE_FS_TEXT_MAX120Longest write text=
KODE_FS_CAT_MAX_DEFAULT, _CEIL4096, 32768 bytesA cat reads this much
KODE_FS_CAT_LINE_MAX200A longer line is cut to 199 and logged
KODE_RECV_CHUNK4096One chunk of file_put, acknowledged with >|ack
Write, test, cat and copy pacers10 ms eachOne step per tick
STORAGE_RECV_LONGSTOP_TICKS, KODE_RECV_IDLE_MS30 s, 5 sfile_put watchdog: long-stop, then between chunks
Card call deadlineDriver bound + 500 msMount, lend, sectors (KDRV_SD_SECTORS_MAX_MS 5 s), format (KDRV_SD_FORMAT_MAX_MS 120 s)

The facade

An app reaches AO_Storage through seven blocking calls: kode_fs_stat(), kode_fs_list(), kode_fs_mkdir(), kode_fs_remove(), kode_fs_rename(), kode_fs_read() and kode_fs_write(), plus kode_sd_lend() and kode_sd_return(). They wait up to 5 s for an answer and return KODE_TIMEOUT after.

KODE_FS_READ_MAX is 4096 bytes, so a read or a write is refused above it. They share one claim, so a second caller gets KODE_DENIED. Never call them from an AO.

Console verbs and settings

sd_mount, sd_unmount, sd_ls, sd_info, fs_mount, fs_unmount, fs_ls, fs_info, ls, cd, pwd, stat, cat, mkdir, rm, rmdir, mv, cp, write, file_put, file_get, file_desc, appdata_ls, appdata_info, appdata_rm, sd, sd_verify and test component=sd|fs. AO_Storage owns no stored setting.

Gotchas

Example code

Write a file to the internal FAT and read it back. With the app running, the log prints read back: hello from the app.

/*
 * storage_roundtrip.c - 2026-10-28 - from Kode with love
 *
 * Writes a short text file to the internal FAT through AO_Storage and reads
 * it back. The facade calls wait for the answer, so they run from app_main()
 * and never from an Active Object. On a Kode Dot the log prints the text.
 */
#include <string.h>
#include "kode.h"
 
#define TAG  "fsdemo"
#define PATH "/storage/hello.txt"
 
/* Write, read back, log. */
void app_main(void)
{
    static const char TEXT[] = "hello from the app";
    char     back[32] = { 0 };
    uint32_t got = 0;
 
    kode_init();
 
    if (kode_fs_write(PATH, TEXT, (uint32_t)strlen(TEXT), false) != KODE_OK) {
        KLOG_E(TAG, "write failed");
        return;
    }
    if (kode_fs_read(PATH, back, sizeof(back) - 1U, &got) == KODE_OK) {
        KLOG_I(TAG, "read back: %s", back);
    }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK