Layer: L7 Application ยท Build flags: PROTOCORE_ENABLE_MNT
What this example teaches
Storage in ProtoCore is two pieces that do two jobs. The mount (protocore_mnt_*) says what is behind the filesystem - a RAM pool in host tests, real flash on the device. The accessor (protocore_fs_*) is how you use it, and it owns the mount root and the resolved path. Features target the accessor, and the application chooses the medium.
Here it is mounted on LittleFS so writes persist across reboots; mounting the RAM backend instead changes nothing in the endpoints.
Mount a backend once, set the root once:
LittleFS.begin(true);
protocore_fs_begin("/");
void protocore_mnt_mount(uint8_t *work)
The file operations are backend-agnostic:
protocore_fs_write_file(name, data, strlen(data));
long n = protocore_fs_read_file(name, buf, cap);
long sz = protocore_fs_size(name);
protocore_fs_remove(name);
Notice what the handlers do not do: they never build a path. They pass the client's ?name= straight through. The accessor joins it onto the root and refuses any .. before storage is touched, so ?name=../../secret is rejected without this sketch containing a line about it. That is the reason path handling lives in one place instead of in every protocol server - there is one guard to get right, and one buffer, rather than one of each per caller.
Build and run
pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPROTOCORE_ENABLE_MNT=1" \
--lib="." examples/L7-Application/Mnt/Mnt.ino
curl "http://<ip>/save?name=greeting&data=hello" # store /greeting
curl "http://<ip>/load?name=greeting" # hello
curl "http://<ip>/size?name=greeting" # 5
curl "http://<ip>/rm?name=greeting" # delete
curl "http://<ip>/load?name=../../secret" # refused by the accessor
Annotated source
The complete sketch (Mnt.ino), reproduced verbatim with added explanatory comments:
#define PROTOCORE_ENABLE_MNT 1
#include "protocore.h"
#include "test/core_setup/hal/esp/esp_mnt_fs.h"
#include <LittleFS.h>
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
PC server;
void setup()
{
Serial.begin(115200);
Physical.wifi->init(SSID, PASSWORD);
while (!Physical.wifi->ready())
delay(250);
uint32_t ip = Physical.link->egress_ip();
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
LittleFS.begin(true);
protocore_fs_begin("/");
const char *name = http_get_query(req, "name");
const char *data = http_get_query(req, "data");
if (!name || !*name || !data)
{
server.send(id, 400, "application/json", "{\"error\":\"name+data\"}");
return;
}
bool ok = protocore_fs_write_file(name, data, strlen(data));
server.send(id, ok ? 200 : 500, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false}");
});
const char *name = http_get_query(req, "name");
if (!name || !*name)
{
server.send(id, 400, "text/plain", "name?");
return;
}
char buf[512];
long n = protocore_fs_read_file(name, buf, sizeof(buf) - 1);
if (n < 0)
{
server.send(id, 404, "text/plain", "not found");
return;
}
buf[n] = '\0';
server.send(id, 200, "text/plain", buf);
});
const char *name = http_get_query(req, "name");
long n = (name && *name) ? protocore_fs_size(name) : -1;
char b[24];
snprintf(b, sizeof(b), "%ld", n);
server.send(id, 200, "text/plain", b);
});
const char *name = http_get_query(req, "name");
bool ok = (name && *name) && protocore_fs_remove(name);
server.send(id, ok ? 200 : 404, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false}");
});
server.begin(80);
}
void loop()
{
server.handle();
}
The filesystem accessor: it owns the mount root, it owns the resolved path, and it is the only way a ...
@ HTTP_GET
Safe, idempotent read.
Layer 1 (Physical) - link bring-up, the interface registry, and live egress reporting.