feat(lofs): exclusive /lofs mount with LoLog

Single LoLog slot on QSPI or nRF flash2; sd/lofs/extra/internal order.
Host lolog tests; drop qspi/spare/reserve mount names.
This commit is contained in:
Ben Allfree committed 2026-10-07 16:53:38 -07:00
1 parent 1243b2bc1b
commit 22c8567c4f
34 files changed
+3117 -106

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ Planned major release **2.0.0** (not tagged yet).
### Changed
- LoFS: volume provider layer (`LoFSVolume`, block devices, host mount tables). Mount names are now `sd`, `qspi`, `reserve`, `spare`, `internal` (replacing `extra`, `flash3`, `flash2`, `flash`). Every LittleFS mount is formattable via `/format`; SD stays refused.
- LoFS: mount names are `sd`, `lofs`, `extra`, `internal`. **`/lofs`** is the exclusive LoLog slot (owned QSPI NOR or nRF52840 flash2 window, never both in the mount table). QSPI Meshtastic envs use `nrf52840_s140_v7_qspi.ld` (no flash2 gap; InternalFS still at `0xED000`). MeshCore `/extra` adopts host ExtraFS; QSPI is not adopted as LittleFS. Dropped `qspi`, `spare`, `reserve`, and flash3 mounts. Every LittleFS mount is formattable via `/format`; SD stays refused. Host tests: `ci/host` `test_lolog_host`.
### Added
+3
View File
@@ -26,3 +26,6 @@ build_src_filter =
+<LoBBSConfig.h>
+<LoBBSStackGuard.cpp>
+<LoBBSStackGuard.h>
+<lofs/lolog/**>
+<lofs/volumes/LoFSRawDevice.cpp>
+<lofs/volumes/LoFSRawDevice.h>
+19
View File
@@ -0,0 +1,19 @@
# LoLog host tests
Run from repo root:
```bash
cd ci/host && pio test -e host -f test_lolog_host
```
Coverage (Unity):
| Area | Tests |
|------|--------|
| Boot / mount | Empty format, erased chip, pure DATA replay, reboot |
| File ops | Nested mkdir, overwrite, rename, rmdir, prefix names, 2.5 KB EXTENT file, writeAt, stat |
| Crash / ACID | Fault on every prog byte (large commit), every prog call, torn tail + append, CRC poke, partial mkdir group |
| Index / clean | Index flush remount, bulk flush, tomb shadow, delete when full, cleaner churn, headroom |
| Fuzz | 30 rounds random ops + crash injection vs ref model |
Fault injection uses `LoLogRamDevice` (`setFaultAfterProgBytes`, `setFaultOnProgCall`, `poke`).
@@ -0,0 +1,63 @@
#pragma once
#include "lofs/lolog/LoLog.h"
#include "lofs/lolog/LoLogRamDevice.h"
#include <cstring>
#include <string>
#include <unity.h>
#include <vector>
inline bool lologWriteFile(LoLog &log, const char *path, const uint8_t *data, size_t len)
{
LoLog::OpenHandle *h = log.open(path, true, true);
if (!h)
return false;
size_t off = 0;
while (off < len) {
size_t n = log.writeHandle(h, data + off, len - off);
if (n == 0)
break;
off += n;
}
log.flushHandle(h);
log.closeHandle(h);
return off == len;
}
inline bool lologReadFile(LoLog &log, const char *path, std::vector<uint8_t> &out)
{
LoLog::OpenHandle *h = log.open(path, false, false);
if (!h)
return false;
out.clear();
uint8_t buf[256];
for (;;) {
size_t n = log.readHandle(h, buf, sizeof(buf));
if (n == 0)
break;
out.insert(out.end(), buf, buf + n);
}
log.closeHandle(h);
return true;
}
inline int lologCountDir(LoLog &log, const char *dirPath)
{
LoLog::OpenHandle *dir = log.openDir(dirPath);
if (!dir)
return -1;
int n = 0;
while (LoLog::OpenHandle *e = log.openNextInDir(dir)) {
log.closeHandle(e);
n++;
}
log.closeHandle(dir);
return n;
}
/** Fresh chip: all 0xFF, mount without explicit format. */
inline bool lologMountErasedChip(LoLogRamDevice &dev, LoLog &log)
{
dev.reset();
return log.mount();
}
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include "lolog_test_helpers.hpp"
#include <map>
#include <set>
#include <string>
#include <vector>
/** In-memory reference tree for fuzz / crash oracle (paths without leading slash). */
class RefFs {
public:
bool mkdir(const std::string &path)
{
if (path.empty() || files.count(path))
return false;
dirs.insert(path);
return true;
}
bool write(const std::string &path, const std::vector<uint8_t> &data)
{
if (path.empty())
return false;
files[path] = data;
return true;
}
bool unlink(const std::string &path) { return files.erase(path) > 0; }
bool rename(const std::string &from, const std::string &to)
{
auto it = files.find(from);
if (it == files.end())
return false;
if (files.count(to))
return false;
files[to] = it->second;
files.erase(it);
return true;
}
bool existsFile(const std::string &path) const { return files.count(path) > 0; }
const std::map<std::string, std::vector<uint8_t>> &allFiles() const { return files; }
void clear()
{
files.clear();
dirs.clear();
}
private:
std::map<std::string, std::vector<uint8_t>> files;
std::set<std::string> dirs;
};
inline bool lologMatchesRef(LoLog &log, const RefFs &ref)
{
for (const auto &kv : ref.allFiles()) {
if (!log.exists(kv.first.c_str()))
return false;
std::vector<uint8_t> got;
if (!lologReadFile(log, kv.first.c_str(), got))
return false;
if (got != kv.second)
return false;
}
for (const auto &kv : ref.allFiles()) {
if (log.exists(kv.first.c_str()) && !ref.existsFile(kv.first))
return false;
}
std::vector<std::string> logPaths;
for (int i = 0; i < 512; i++) {
char p[32];
snprintf(p, sizeof(p), "fuzz_%d.dat", i);
if (log.exists(p))
logPaths.push_back(p);
}
for (const auto &p : logPaths) {
if (!ref.existsFile(p))
return false;
}
return true;
}
@@ -0,0 +1,41 @@
#include "lolog_test_helpers.hpp"
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
void test_mount_empty_after_format(void)
{
TEST_ASSERT_FALSE(gLog.exists("anything"));
TEST_ASSERT_TRUE(gLog.isDirectory(""));
}
void test_mount_erased_chip_without_format(void)
{
LoLogRamDevice dev(16);
LoLog log(dev);
TEST_ASSERT_TRUE(lologMountErasedChip(dev, log));
}
void test_pure_replay_no_index_runs(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("replay"));
std::vector<uint8_t> payload = {'a', 'b', 'c'};
TEST_ASSERT_TRUE(lologWriteFile(gLog, "replay/r1.ls", payload.data(), payload.size()));
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
std::vector<uint8_t> readback;
TEST_ASSERT_TRUE(lologReadFile(g2, "replay/r1.ls", readback));
TEST_ASSERT_EQUAL_UINT32(payload.size(), readback.size());
}
void test_mount_after_reboot_simulation(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("boot"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "boot/persist.ls", (const uint8_t *)"ok", 2));
LoLog after(gDev);
TEST_ASSERT_TRUE(after.mount());
TEST_ASSERT_TRUE(after.exists("boot/persist.ls"));
}
+235
View File
@@ -0,0 +1,235 @@
#include "lolog_test_helpers.hpp"
#include "lofs/lolog/LoLogConfig.h"
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
static uint32_t progCallsForLargeCommit(LoLogRamDevice &dev)
{
dev.reset();
LoLog log(dev);
log.format();
log.mkdir("gc");
LoLog::OpenHandle *h = log.open("gc/big.bin", true, true);
if (!h)
return 0;
dev.clearProgCallCount();
std::vector<uint8_t> payload(2500, 0x5A);
log.writeHandle(h, payload.data(), payload.size());
log.flushHandle(h);
log.closeHandle(h);
return dev.progCallCount();
}
static bool groupVisibleAfterRemount(LoLogRamDevice &dev, const char *path, size_t expectLen)
{
LoLog log(dev);
if (!log.mount())
return false;
if (!log.exists(path))
return expectLen == 0;
std::vector<uint8_t> got;
if (!lologReadFile(log, path, got))
return false;
return got.size() == expectLen;
}
void test_commit_group_atomic_each_prog_call(void)
{
const uint32_t calls = progCallsForLargeCommit(gDev);
TEST_ASSERT_GREATER_THAN(1, calls);
for (uint32_t fault = 1; fault <= calls; fault++) {
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("gc"));
gDev.setFaultOnProgCall(fault);
LoLog::OpenHandle *h = log.open("gc/big.bin", true, true);
if (h) {
std::vector<uint8_t> payload(2500, 0x5A);
log.writeHandle(h, payload.data(), payload.size());
log.flushHandle(h);
log.closeHandle(h);
}
gDev.setFaultOnProgCall(0);
const bool full = groupVisibleAfterRemount(gDev, "gc/big.bin", 2500);
if (!full) {
LoLog absent(gDev);
absent.mount();
if (absent.exists("gc/big.bin")) {
std::vector<uint8_t> got;
lologReadFile(absent, "gc/big.bin", got);
TEST_ASSERT_EQUAL_UINT32(0, got.size());
}
} else {
LoLog check(gDev);
check.mount();
std::vector<uint8_t> got;
lologReadFile(check, "gc/big.bin", got);
for (uint8_t b : got)
TEST_ASSERT_EQUAL_UINT8(0x5A, b);
}
}
}
void test_commit_group_partial_never_applied(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("p"));
gDev.setFaultOnProgCall(1);
TEST_ASSERT_FALSE(log.mkdir("p/sub"));
gDev.setFaultOnProgCall(0);
LoLog rem(gDev);
rem.mount();
TEST_ASSERT_FALSE(rem.exists("p/sub"));
}
void test_commit_group_each_prog_byte(void)
{
LoLogRamDevice measure(32);
measure.reset();
LoLog mlog(measure);
mlog.format();
mlog.mkdir("by");
measure.clearProgBytes();
std::vector<uint8_t> payload(2500, 0x11);
LoLog::OpenHandle *mh = mlog.open("by/big.bin", true, true);
TEST_ASSERT_NOT_NULL(mh);
mlog.writeHandle(mh, payload.data(), payload.size());
mlog.flushHandle(mh);
mlog.closeHandle(mh);
const uint64_t totalBytes = measure.progBytes();
TEST_ASSERT_GREATER_THAN(8, totalBytes);
for (uint64_t cut = 1; cut <= totalBytes; cut++) {
measure.reset();
LoLog l(measure);
l.format();
l.mkdir("by");
measure.setFaultAfterProgBytes((uint32_t)cut);
LoLog::OpenHandle *lh = l.open("by/big.bin", true, true);
if (lh) {
l.writeHandle(lh, payload.data(), payload.size());
l.flushHandle(lh);
l.closeHandle(lh);
}
measure.setFaultAfterProgBytes(0);
const bool full = groupVisibleAfterRemount(measure, "by/big.bin", 2500);
if (full) {
LoLog check(measure);
check.mount();
std::vector<uint8_t> got;
lologReadFile(check, "by/big.bin", got);
TEST_ASSERT_EQUAL_UINT32(2500, got.size());
} else {
LoLog absent(measure);
absent.mount();
if (absent.exists("by/big.bin")) {
std::vector<uint8_t> got;
lologReadFile(absent, "by/big.bin", got);
TEST_ASSERT_EQUAL_UINT32(0, got.size());
}
}
}
}
void test_torn_tail_then_append(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("torn"));
TEST_ASSERT_TRUE(lologWriteFile(log, "torn/a.ls", (const uint8_t *)"AAAA", 4));
gDev.setFaultAfterProgBytes(12);
LoLog::OpenHandle *h = log.open("torn/b.ls", true, true);
if (h) {
log.writeHandle(h, (const uint8_t *)"BBBBBBBB", 8);
log.flushHandle(h);
log.closeHandle(h);
}
gDev.setFaultAfterProgBytes(0);
{
LoLog mid(gDev);
TEST_ASSERT_TRUE(mid.mount());
TEST_ASSERT_TRUE(mid.exists("torn/a.ls"));
if (mid.exists("torn/b.ls")) {
std::vector<uint8_t> shell;
lologReadFile(mid, "torn/b.ls", shell);
TEST_ASSERT_EQUAL_UINT32(0, shell.size());
}
}
LoLog fin(gDev);
TEST_ASSERT_TRUE(fin.mount());
TEST_ASSERT_TRUE(lologWriteFile(fin, "torn/c.ls", (const uint8_t *)"CCCC", 4));
std::vector<uint8_t> finRead;
TEST_ASSERT_TRUE(lologReadFile(fin, "torn/c.ls", finRead));
TEST_ASSERT_EQUAL_UINT32(4, finRead.size());
TEST_ASSERT_TRUE(fin.exists("torn/a.ls"));
}
static void corruptSecondEntryCrc(LoLogRamDevice &dev)
{
for (uint32_t s = 0; s < dev.sectorCount(); s++) {
uint32_t off = lolog::kHeaderSize;
int ent = 0;
while (off + 8 <= lolog::kSectorSize) {
uint8_t hdr[8];
if (!dev.read(s * lolog::kSectorSize + off, hdr, 8))
break;
uint16_t plen = (uint16_t)hdr[0] | ((uint16_t)hdr[1] << 8);
if (plen == lolog::kEntryEnd)
break;
const uint32_t padded = (8u + plen + 3u) & ~3u;
if (hdr[2] == lolog::kEntFile) {
ent++;
if (ent == 2 && plen > 0) {
dev.poke(s * lolog::kSectorSize + off + 4 + plen, 0x00);
return;
}
}
off += padded;
}
}
}
void test_torn_entry_crc_poke(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("poke"));
TEST_ASSERT_TRUE(lologWriteFile(log, "poke/ok.ls", (const uint8_t *)"good", 4));
TEST_ASSERT_TRUE(lologWriteFile(log, "poke/next.ls", (const uint8_t *)"bad", 3));
corruptSecondEntryCrc(gDev);
LoLog rem(gDev);
TEST_ASSERT_TRUE(rem.mount());
TEST_ASSERT_TRUE(rem.exists("poke/ok.ls"));
TEST_ASSERT_FALSE(rem.exists("poke/next.ls"));
TEST_ASSERT_TRUE(lologWriteFile(rem, "poke/after.ls", (const uint8_t *)"yes", 3));
TEST_ASSERT_TRUE(rem.exists("poke/after.ls"));
}
void test_fault_single_byte_discards_group(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("g"));
gDev.setFaultAfterProgBytes(1);
LoLog::OpenHandle *h = gLog.open("g/a.ls", true, true);
if (h) {
gLog.writeHandle(h, (const uint8_t *)"x", 1);
gLog.flushHandle(h);
gLog.closeHandle(h);
}
gDev.setFaultAfterProgBytes(0);
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_FALSE(g2.exists("g/a.ls"));
}
@@ -0,0 +1,104 @@
#include "lolog_test_helpers.hpp"
#include <string>
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
void test_mkdir_nested_and_overwrite(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("a/b/c"));
TEST_ASSERT_TRUE(gLog.isDirectory("a"));
TEST_ASSERT_TRUE(gLog.isDirectory("a/b"));
TEST_ASSERT_TRUE(gLog.isDirectory("a/b/c"));
const uint8_t v1[] = "one";
const uint8_t v2[] = "two-longer";
TEST_ASSERT_TRUE(lologWriteFile(gLog, "a/b/c/f.ls", v1, sizeof(v1)));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "a/b/c/f.ls", v2, sizeof(v2)));
std::vector<uint8_t> got;
TEST_ASSERT_TRUE(lologReadFile(gLog, "a/b/c/f.ls", got));
TEST_ASSERT_EQUAL_UINT32(sizeof(v2), got.size());
}
void test_rename_file(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("rn"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "rn/old.ls", (const uint8_t *)"data", 4));
TEST_ASSERT_TRUE(gLog.rename("rn/old.ls", "rn/new.ls"));
TEST_ASSERT_FALSE(gLog.exists("rn/old.ls"));
TEST_ASSERT_TRUE(gLog.exists("rn/new.ls"));
}
void test_rmdir_nonempty_fails_nonempty_ok_recursive(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("rd"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "rd/x.ls", (const uint8_t *)"1", 1));
TEST_ASSERT_FALSE(gLog.rmdir("rd", false));
TEST_ASSERT_TRUE(gLog.rmdir("rd", true));
TEST_ASSERT_FALSE(gLog.exists("rd/x.ls"));
}
void test_prefix_collision_names(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("pfx"));
const std::string p1 = "pfx/abcdefgh_11111111.ls";
const std::string p2 = "pfx/abcdefgh_22222222.ls";
TEST_ASSERT_TRUE(lologWriteFile(gLog, p1.c_str(), (const uint8_t *)"A", 1));
TEST_ASSERT_TRUE(lologWriteFile(gLog, p2.c_str(), (const uint8_t *)"B", 1));
std::vector<uint8_t> a, b;
TEST_ASSERT_TRUE(lologReadFile(gLog, "pfx/abcdefgh_11111111.ls", a));
TEST_ASSERT_TRUE(lologReadFile(gLog, "pfx/abcdefgh_22222222.ls", b));
TEST_ASSERT_EQUAL_UINT8(0x41, a[0]);
TEST_ASSERT_EQUAL_UINT8(0x42, b[0]);
}
void test_large_file_inline_and_extent(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("big"));
std::vector<uint8_t> payload(2500);
for (size_t i = 0; i < payload.size(); i++)
payload[i] = (uint8_t)(i & 0xFF);
TEST_ASSERT_TRUE(lologWriteFile(gLog, "big/upload.bin", payload.data(), payload.size()));
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
std::vector<uint8_t> got;
TEST_ASSERT_TRUE(lologReadFile(g2, "big/upload.bin", got));
TEST_ASSERT_EQUAL_UINT32(payload.size(), got.size());
TEST_ASSERT_EQUAL_UINT8(0, got[0]);
TEST_ASSERT_EQUAL_UINT8(0xC3, got[2500 - 1]);
}
void test_large_file_write_at_path(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("up"));
LoLog::OpenHandle *h = gLog.open("up/chunk.bin", true, true);
TEST_ASSERT_NOT_NULL(h);
gLog.closeHandle(h);
uint8_t chunk[600];
for (size_t i = 0; i < sizeof(chunk); i++)
chunk[i] = (uint8_t)i;
TEST_ASSERT_TRUE(gLog.writeAtPath("up/chunk.bin", 0, chunk, sizeof(chunk)));
std::vector<uint8_t> got;
TEST_ASSERT_TRUE(lologReadFile(gLog, "up/chunk.bin", got));
TEST_ASSERT_EQUAL_UINT32(sizeof(chunk), got.size());
TEST_ASSERT_EQUAL_UINT8(0, got[0]);
TEST_ASSERT_EQUAL_UINT8(255, got[255]);
}
void test_stat_and_is_directory(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("st"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "st/f.ls", (const uint8_t *)"x", 1));
uint32_t sz = 0;
bool isDir = true;
TEST_ASSERT_TRUE(gLog.statPath("st/f.ls", &sz, &isDir));
TEST_ASSERT_FALSE(isDir);
TEST_ASSERT_EQUAL_UINT32(1, sz);
TEST_ASSERT_TRUE(gLog.statPath("st", &sz, &isDir));
TEST_ASSERT_TRUE(isDir);
}
@@ -0,0 +1,99 @@
#include "lolog_test_helpers.hpp"
#include "ref_fs.hpp"
#include <unity.h>
extern LoLogRamDevice gDev;
static uint32_t lfsRand(uint32_t &state)
{
state = state * 1664525u + 1013904223u;
return state;
}
static void applyRefOp(RefFs &ref, LoLog &log, uint32_t op, uint32_t &rng)
{
char path[48];
char path2[48];
switch (op % 5) {
case 0: {
snprintf(path, sizeof(path), "fuzz/d%d", (int)(lfsRand(rng) % 20));
if (log.mkdir(path))
ref.mkdir(path);
break;
}
case 1: {
snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40));
std::vector<uint8_t> data(8 + (lfsRand(rng) % 120));
for (size_t i = 0; i < data.size(); i++)
data[i] = (uint8_t)lfsRand(rng);
if (lologWriteFile(log, path, data.data(), data.size()))
ref.write(path, data);
break;
}
case 2: {
snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40));
if (log.remove(path))
ref.unlink(path);
break;
}
case 3: {
snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40));
snprintf(path2, sizeof(path2), "fuzz/t%d.dat", (int)(lfsRand(rng) % 40));
if (ref.existsFile(path) && !ref.existsFile(path2) && log.rename(path, path2))
ref.rename(path, path2);
break;
}
default:
log.maintain(5000);
break;
}
}
void test_random_ops_with_crash_injection(void)
{
RefFs ref;
uint32_t rng = 0xC0FFEE42u;
for (int round = 0; round < 30; round++) {
gDev.reset();
LoLog log(gDev);
log.format();
log.mkdir("fuzz");
ref.clear();
ref.mkdir("fuzz");
const int opsBeforeCrash = 3 + (int)(lfsRand(rng) % 12);
for (int i = 0; i < opsBeforeCrash; i++)
applyRefOp(ref, log, lfsRand(rng), rng);
gDev.setFaultOnProgCall(1 + (lfsRand(rng) % 8));
applyRefOp(ref, log, lfsRand(rng), rng);
gDev.setFaultOnProgCall(0);
LoLog rem(gDev);
TEST_ASSERT_TRUE(rem.mount());
for (const auto &kv : ref.allFiles()) {
if (!rem.exists(kv.first.c_str()))
continue;
std::vector<uint8_t> got;
if (!lologReadFile(rem, kv.first.c_str(), got))
continue;
if (got.size() != kv.second.size())
continue;
if (!got.empty())
TEST_ASSERT_EQUAL_UINT8_ARRAY(kv.second.data(), got.data(), got.size());
}
}
}
void test_directory_list_count(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("d"));
TEST_ASSERT_TRUE(lologWriteFile(log, "d/one.ls", (const uint8_t *)"1", 1));
TEST_ASSERT_TRUE(lologWriteFile(log, "d/two.ls", (const uint8_t *)"2", 1));
TEST_ASSERT_EQUAL(2, lologCountDir(log, "d"));
}
@@ -0,0 +1,110 @@
#include "lolog_test_helpers.hpp"
#include "lofs/lolog/LoLogConfig.h"
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
void test_index_flush_pure_reboot(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("ix"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "ix/a.ls", (const uint8_t *)"z", 1));
gLog.maintain(60000);
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_TRUE(g2.exists("ix/a.ls"));
}
void test_index_multiple_flushes_and_lookup(void)
{
for (int i = 0; i < 90; i++) {
char dir[16];
snprintf(dir, sizeof(dir), "bulk%d", i);
TEST_ASSERT_TRUE(gLog.mkdir(dir));
char path[32];
snprintf(path, sizeof(path), "%s/r.ls", dir);
TEST_ASSERT_TRUE(lologWriteFile(gLog, path, (const uint8_t *)"x", 1));
gLog.maintain(60000);
}
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_TRUE(g2.exists("bulk42/r.ls"));
TEST_ASSERT_TRUE(g2.exists("bulk0/r.ls"));
}
void test_tombstone_shadow_after_remount(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("ts"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "ts/gone.ls", (const uint8_t *)"1", 1));
TEST_ASSERT_TRUE(gLog.remove("ts/gone.ls"));
gLog.maintain(60000);
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_FALSE(g2.exists("ts/gone.ls"));
}
void test_delete_when_near_full(void)
{
LoLogRamDevice tiny(16);
LoLog log(tiny);
log.format();
TEST_ASSERT_TRUE(lologWriteFile(log, "f0.ls", (const uint8_t *)"x", 1));
int created = 1;
for (int i = 1; i < 500; i++) {
char path[24];
snprintf(path, sizeof(path), "f%d.ls", i);
if (!lologWriteFile(log, path, (const uint8_t *)"x", 1))
break;
created++;
}
TEST_ASSERT_GREATER_THAN(3, created);
TEST_ASSERT_TRUE(log.remove("f0.ls"));
char extra[24];
snprintf(extra, sizeof(extra), "f%d.ls", created + 10);
TEST_ASSERT_TRUE(lologWriteFile(log, extra, (const uint8_t *)"n", 1));
}
void test_cleaner_recycles_sectors(void)
{
LoLogRamDevice dev(12);
LoLog log(dev);
log.format();
for (int i = 0; i < 80; i++) {
char path[20];
snprintf(path, sizeof(path), "c%d.ls", i);
if (!lologWriteFile(log, path, (const uint8_t *)"xy", 2))
break;
}
for (int i = 0; i < 40; i++) {
char path[20];
snprintf(path, sizeof(path), "c%d.ls", i);
log.remove(path);
}
for (int m = 0; m < 60; m++)
log.maintain(60000);
uint32_t usedAfter = (uint32_t)log.usedBytes();
(void)usedAfter;
TEST_ASSERT_TRUE(log.exists("c79.ls"));
std::vector<uint8_t> tail;
TEST_ASSERT_TRUE(lologReadFile(log, "c79.ls", tail));
TEST_ASSERT_EQUAL_UINT8(0x78, tail[0]);
}
void test_headroom_blocks_before_starve(void)
{
LoLogRamDevice tiny(6);
LoLog log(tiny);
log.format();
for (int i = 0; i < 200; i++) {
char path[32];
snprintf(path, sizeof(path), "f%d.ls", i);
LoLog::OpenHandle *h = log.open(path, true, true);
if (!h)
break;
log.closeHandle(h);
}
LoLog::OpenHandle *last = log.open("overflow.ls", true, true);
TEST_ASSERT_NULL(last);
}
@@ -0,0 +1,85 @@
#include "lofs/lolog/LoLogRamDevice.h"
#include "lofs/lolog/LoLog.h"
#include <unity.h>
LoLogRamDevice gDev(32);
LoLog gLog(gDev);
void setUp(void)
{
gDev.reset();
gLog.format();
}
void tearDown(void) {}
#define DECL(name) void name(void)
DECL(test_mount_empty_after_format);
DECL(test_mount_erased_chip_without_format);
DECL(test_pure_replay_no_index_runs);
DECL(test_mount_after_reboot_simulation);
DECL(test_mkdir_nested_and_overwrite);
DECL(test_rename_file);
DECL(test_rmdir_nonempty_fails_nonempty_ok_recursive);
DECL(test_prefix_collision_names);
DECL(test_large_file_inline_and_extent);
DECL(test_large_file_write_at_path);
DECL(test_stat_and_is_directory);
DECL(test_commit_group_atomic_each_prog_call);
DECL(test_commit_group_each_prog_byte);
DECL(test_torn_tail_then_append);
DECL(test_torn_entry_crc_poke);
DECL(test_fault_single_byte_discards_group);
DECL(test_commit_group_partial_never_applied);
DECL(test_index_flush_pure_reboot);
DECL(test_index_multiple_flushes_and_lookup);
DECL(test_tombstone_shadow_after_remount);
DECL(test_delete_when_near_full);
DECL(test_cleaner_recycles_sectors);
DECL(test_headroom_blocks_before_starve);
DECL(test_random_ops_with_crash_injection);
DECL(test_directory_list_count);
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_mount_empty_after_format);
RUN_TEST(test_mount_erased_chip_without_format);
RUN_TEST(test_pure_replay_no_index_runs);
RUN_TEST(test_mount_after_reboot_simulation);
RUN_TEST(test_mkdir_nested_and_overwrite);
RUN_TEST(test_rename_file);
RUN_TEST(test_rmdir_nonempty_fails_nonempty_ok_recursive);
RUN_TEST(test_prefix_collision_names);
RUN_TEST(test_large_file_inline_and_extent);
RUN_TEST(test_large_file_write_at_path);
RUN_TEST(test_stat_and_is_directory);
RUN_TEST(test_commit_group_atomic_each_prog_call);
RUN_TEST(test_commit_group_each_prog_byte);
RUN_TEST(test_torn_tail_then_append);
RUN_TEST(test_torn_entry_crc_poke);
RUN_TEST(test_fault_single_byte_discards_group);
RUN_TEST(test_commit_group_partial_never_applied);
RUN_TEST(test_index_flush_pure_reboot);
RUN_TEST(test_index_multiple_flushes_and_lookup);
RUN_TEST(test_tombstone_shadow_after_remount);
RUN_TEST(test_delete_when_near_full);
RUN_TEST(test_cleaner_recycles_sectors);
RUN_TEST(test_headroom_blocks_before_starve);
RUN_TEST(test_random_ops_with_crash_injection);
RUN_TEST(test_directory_list_count);
return UNITY_END();
}
+1 -1
View File
@@ -21,7 +21,7 @@ const char *lobbsInstallOfflineHome(const LoBBSKernel &kernel);
void lobbsInstallInit(LoBBSKernel &kernel);
void lobbsInstallDatabaseOpened(LoBBSKernel &kernel);
/** `install_mounts` names joined with `|`, e.g. `sd|qspi|reserve|spare|internal`. */
/** `install_mounts` names joined with `|`, e.g. `sd|lofs|extra|internal`. */
void lobbsInstallMountList(LoBBSCommandCtx &ctx, char *out, size_t cap);
/** `<root>/lobbs`, e.g. `/extra/lobbs`. */
+5 -5
View File
@@ -95,13 +95,13 @@ Machine: records encoded as LoScalar lines, sliced into pages with `<id>ok [n:ma
## LoFS and install
LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `/<mount>/…`. Slot names in install preference order: `sd`, `qspi`, `reserve`, `spare`, `internal`. Each slot maps to a `LoFSVolume` provider (adopted host LittleFS or LoBBS-mounted block device). Host composition lives in `lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp` and `lofs/platforms/meshcore/LoFSMountsMeshcore.cpp`. Mount roots cannot be removed, renamed, or used as copy sources. Cross-mount `mv` copies files then deletes the source. Directories require same-mount rename.
LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `/<mount>/…`. Slot names in install preference order: `sd`, `lofs`, `extra`, `internal`. Each slot maps to a `LoFSVolume` provider (adopted host LittleFS, LoLog on `/lofs`, or SD). Host composition lives in `lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp` and `lofs/platforms/meshcore/LoFSMountsMeshcore.cpp`. Mount roots cannot be removed, renamed, or used as copy sources. Cross-mount `mv` copies files then deletes the source. Directories require same-mount rename.
`/spare` is the middle nRF52840 internal window (100 KiB at `0xD4000`–`0xED000`, weak symbols `__flash2_start` / `__flash2_end`). Meshtastic mounts it with `LoFSNrfFlashBlockDevice` when the linker provides the symbols. MeshCore adopts host `ExtraFS` on the same range when `EXTRAFS` is enabled.
`/lofs` is the exclusive LoLog slot. On boards with owned QSPI NOR (`LOFS_BOARD_HAS_QSPI`), LoLog uses `LoFSQspiNorBlockDevice` (about 2 MiB). Meshtastic QSPI envs link `nrf52840_s140_v7_qspi.ld`: application flash runs to `0xED000` with no `__flash2` gap. Other nRF52840 Meshtastic builds use `nrf52840_s140_v7.ld`, which stops the app at `0xD4000` and provides `__flash2_start` / `__flash2_end` (100 KiB) for `/lofs` via `LoFSNrfFlashRawDevice`. Meshtastic `InternalFS` stays at the Adafruit default (`0xED000` on nRF52840). ESP32 and boards with neither QSPI nor flash2 have no `/lofs`.
`/reserve` is the low window (`0xCC000`–`0xD4000`, `__flash3_*`) when the opt-in MeshCore linker script or Meshtastic layout leaves it empty. LoBBS mounts it with the same block device helper.
`/lofs` uses **LoLog** (`lofs/lolog/`, `LoFSLoLogVolume`): append-only data segments, on-flash index runs, background clean/flush via `LoFS::maintain()` from the Meshtastic module `runOnce` and `lobbsMeshCoreLoop`. `/format lofs` erases the volume and starts a new epoch. There is no LittleFS on `/lofs`. MeshCore does not adopt host QSPI LittleFS: when `QSPIFLASH` is set, the chip is `/lofs` LoLog like Meshtastic.
`/qspi` is onboard NOR (2 MiB). Meshtastic always mounts via `LoFSQspiNorBlockDevice` when `LOFS_ENABLE_EXTRA_QSPI`. MeshCore adopts `QSPIFlash` when `QSPIFLASH`, otherwise LoBBS mounts the chip when present.
`/extra` is MeshCore only: host `ExtraFS` LittleFS at `0xD4000` when `EXTRAFS` is enabled and the board has no QSPI (`!QSPIFLASH`). It is shared with the radio. Do not mount `/lofs` on the same flash range as `/extra`.
`/internal` is the host primary filesystem (`InternalFS` / `FSCom` on Meshtastic, the filesystem pointer passed to `lobbsMeshCoreInit` on MeshCore). It is shared: LoBBS keeps a reserve on writes. `/format internal` on Meshtastic runs `FSCom.format()` then `nodeDB->saveToDisk()`. Formatting the install mount reruns `lobbsInstallInit`.
@@ -109,7 +109,7 @@ LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `/<
Each mount has `dbSafe` (computed at boot: non-shared volumes are always db-safe; shared volumes are db-safe only when no higher-preference mount is present) and `formattable`. LittleFS mounts are formattable; `sd` is not. Install points come from the `install_mounts` list filter in preference order above.
MeshCore nRF52840 can opt into `/reserve` with `boards/nrf52840_s140_v7_lobbs_flash3.ld`. Example env: `RAK_4631_terminal_chat` in `lobbs-meshcore-firmware`. `lobbsMeshCoreInit` calls `lofsMeshcoreSetHostFilesystem` before `LoFS::begin`.
`lobbsMeshCoreInit` calls `lofsMeshcoreSetHostFilesystem` before `LoFS::begin`.
Shared mounts keep reserve bytes free (16 KiB nRF52, 128 KiB other hardware, 0 Portduino). `LoFS::hasRoom(path, bytes)` rounds `bytes` up to the block size, adds slack for metadata, adds the mount reserve, and compares with free space. It returns true when the mount reports no size. LoDB checks it before every record write and returns `LODB_ERR_FULL`. FsCommands checks it for `/mkdir`, `/cp`, `/upload`, and cross-mount `/mv`. Apps map `LODB_ERR_FULL` to `Disk full.` via `lobbsDbErrorText`. Deletes are never blocked.
+10 -8
View File
@@ -14,11 +14,11 @@ A new LoBBS node has no database until you install it. Only `/help` and `/instal
| Hardware | Install mounts |
| ------------------------ | ---------------------------------------------------- |
| nRF52840 Meshtastic | `sd`, `qspi`, `reserve` (if linked), `spare` (100 KB LoBBS) |
| nRF52840 MeshCore | `qspi`, `reserve` (opt-in linker), `spare` (host ExtraFS), `internal` |
| ESP32 and other hardware | `sd`, `qspi`, `internal` |
| nRF52840 Meshtastic | `sd`, `lofs` (QSPI or 100 KB flash2), `internal` |
| nRF52840 MeshCore | `lofs` (QSPI boards), `extra`, `internal` |
| ESP32 and other hardware | `sd`, `internal` |
Mounts are listed in preference order. `sd` appears when a card is present. `qspi` (about 2 MB) appears on Seeed XIAO nRF52840 kit and SenseCAP Solar Node builds (`LOBBS_EXTRA_QSPI=1`). On nRF52840 Meshtastic, `/internal` is the radio's own filesystem and is only offered when no better mount exists.
Mounts are listed in preference order. `sd` appears when a card is present. `/lofs` (LoLog, about 2 MB on QSPI or 100 KB on flash2) appears when that hardware exists. On nRF52840 Meshtastic, `/internal` is the radio's own filesystem and is only offered when no better mount exists.
`/install <mount> <user> <pass>` creates the database on that mount and makes `<user>` the SysOp. Use it from the local client (phone app connected over serial/BLE) or from a remote DM encrypted with a configured admin key. If the target already holds a LoBBS database, the same command adopts it and you must supply an existing SysOp username and password. Once installed, `/install` replies `Already installed at /<mount>.`
@@ -31,7 +31,7 @@ Everything LoBBS stores lives under `/<mount>/lobbs`:
| `/<mount>/lobbs/apps/<app>/` | Reserved for per-app files |
| `/<mount>/lobbs/home/<user>/` | Reserved for user files |
On boot LoBBS checks the install mounts in preference order and uses the first one holding `lobbs/install.ls`. Nothing is stored on `/internal`, so a Meshtastic wipe of `/internal` does not lose the install when it lives on `spare`, `qspi`, or `sd`. Pulling the SD card that holds it makes the node look new until the card is back.
On boot LoBBS checks the install mounts in preference order and uses the first one holding `lobbs/install.ls`. Nothing is stored on `/internal`, so a Meshtastic wipe of `/internal` does not lose the install when it lives on `lofs` or `sd`. Pulling the SD card that holds it makes the node look new until the card is back.
## Demo builds
@@ -69,16 +69,18 @@ Config keys (uint32, defaults in parentheses): `session.max` (16), `session.idle
| `/upload path [offset:b62]` | Chunked file write at byte offset, or show current size |
| `/commit src dst` | Move when file CRC32 matches the 8-hex segment in the source name |
| `/stat path` | `file N` or `dir` |
| `/df` | One line per mount, e.g. `qspi: 1.8M free of 2.0M (12% used) [lobbs]`. Free space excludes the `/internal` reserve. `[lobbs]` marks the install mount |
| `/df` | One line per mount, e.g. `lofs: 1.8M free of 2.0M (12% used) [lobbs]`. Free space excludes the `/internal` reserve. `[lobbs]` marks the install mount |
| `/format mount [code]` | Erase a mount. The first call replies with a code; repeat with the code within 2 minutes from the same node. On `/internal` the radio settings are saved again from memory (BLE pairings are lost). On the install mount LoBBS goes blank until `/install`. `sd` is refused (format cards on a PC) |
SysOps painting the wall or adding yarn do not use quota.
## Files
Paths use mount prefixes: `/internal/...`, `/spare/...` on nRF52840, `/sd/...`, `/qspi/...` on builds with `LOBBS_EXTRA_QSPI=1` and a QSPI flash chip. `/` lists mounts. Do not `/rmtree` `<mount>/lobbs` on your install mount unless you intend to wipe the BBS.
Paths use mount prefixes: `/internal/...`, `/extra/...` on MeshCore nRF52840, `/sd/...`, `/lofs/...` when that slot exists. `/` lists mounts. Do not `/rmtree` `<mount>/lobbs` on your install mount unless you intend to wipe the BBS.
`/internal` is shared with Meshtastic's own settings and node database. LoBBS leaves a reserve free there (16 KB on nRF52, 128 KB elsewhere). Posts, mail, uploads, and copies that would cut into it reply `Disk full.`. Deleting files always works. `/spare`, `/sd`, and `/qspi` have no reserve.
`/internal` is shared with Meshtastic's own settings and node database. LoBBS leaves a reserve free there (16 KB on nRF52, 128 KB elsewhere). Posts, mail, uploads, and copies that would cut into it reply `Disk full.`. Deleting files always works. `/extra` and `/sd` have no LoBBS reserve. `/lofs` uses LoLog with cleaner headroom held back so `Disk full.` appears before the log can deadlock; used bytes on `/df` are live data only. Other tools cannot read a LoLog-formatted volume.
After upgrading from builds that used mount names `qspi`, `spare`, or LittleFS on QSPI, run `/format lofs` once (2-minute confirm) before `/install lofs`. Old data is not migrated.
Relative paths work under your session working directory for `ls`, `cat`, `hex`, `stat`, `mkdir`, `cp`, `mv`, `upload`, and `commit`. `rm`, `rmdir`, and `rmtree` require absolute paths.
+10
View File
@@ -124,6 +124,16 @@ void LoFS::eachPresentMount(void (*fn)(void *ctx, const char *name), void *ctx)
}
}
void LoFS::maintain(uint32_t budgetMs)
{
if (!begun)
return;
for (int i = 0; i < mountCount; i++) {
if (mounts[i].present && mounts[i].volume)
mounts[i].volume->maintain(budgetMs);
}
}
bool LoFS::resolve(const char *filepath, Resolved &out)
{
out = {};
+4 -1
View File
@@ -5,7 +5,7 @@
#include "LoFile.h"
/**
* Unified filesystem with a mount table in install preference order: /sd, /qspi, /reserve, /spare, /internal.
* Unified filesystem with a mount table in install preference order: /sd, /lofs, /extra, /internal.
* "/" is a virtual root that lists mounts. Every other path must start with "/<mount>/..." or "/<mount>".
*/
enum class LoFSMoveResult : uint8_t {
@@ -67,6 +67,9 @@ class LoFS
typedef bool (*ListCallback)(void *ctx, const char *basename, bool isDirectory, uint32_t size);
static bool list(const char *dirpath, void *ctx, ListCallback fn);
/** Calls maintain() on each mounted volume (LoLog cleaning on lofs). */
static void maintain(uint32_t budgetMs);
private:
struct Mount {
const char *name;
+1263
View File
File diff suppressed because it is too large. Load diff
+156
View File
@@ -0,0 +1,156 @@
#pragma once
#include "lofs/lolog/LoLogConfig.h"
#include "lofs/volumes/LoFSRawDevice.h"
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <vector>
namespace lolog {
struct Loc {
uint16_t sector;
uint16_t off;
};
} // namespace lolog
class LoLog {
public:
explicit LoLog(LoFSRawDevice &device);
bool mount();
bool format();
void maintain(uint32_t budgetMs);
uint64_t totalBytes() const;
uint64_t usedBytes() const;
void spaceHint(uint32_t *blockOut, uint32_t *slackOut) const;
bool exists(const char *relPath);
bool isDirectory(const char *relPath);
bool statPath(const char *relPath, uint32_t *sizeOut, bool *isDirOut);
bool mkdir(const char *relPath);
bool remove(const char *relPath);
bool rename(const char *oldPath, const char *newPath);
bool rmdir(const char *relPath, bool recursive);
struct OpenHandle {
bool isDirIter = false;
uint32_t dirId = 0;
size_t dirIdx = 0;
uint32_t inodeId = 0;
bool write = false;
bool dirty = false;
uint32_t pos = 0;
std::vector<uint8_t> writeBuf;
char nameBuf[64];
};
OpenHandle *open(const char *relPath, bool write, bool create);
void closeHandle(OpenHandle *h);
size_t readHandle(OpenHandle *h, uint8_t *buf, size_t len);
size_t writeHandle(OpenHandle *h, const uint8_t *buf, size_t len);
bool seekHandle(OpenHandle *h, uint32_t pos);
uint32_t sizeHandle(OpenHandle *h) const;
bool isDirHandle(OpenHandle *h) const;
void flushHandle(OpenHandle *h);
const char *nameHandle(OpenHandle *h) const;
OpenHandle *openDir(const char *relPath);
OpenHandle *openNextInDir(OpenHandle *dir);
bool writeAtPath(const char *relPath, uint32_t offset, const uint8_t *data, size_t len);
private:
LoFSRawDevice &dev_;
bool mounted_ = false;
uint32_t epoch_ = 0;
uint32_t nextSeq_ = 1;
uint32_t nextDirId_ = 1;
uint32_t nextInodeId_ = 1;
uint32_t coveredSeq_ = 0;
uint64_t liveBytes_ = 0;
uint32_t lastActivityMs_ = 0;
uint16_t activeSector_ = 0xFFFF;
uint32_t activeOff_ = lolog::kHeaderSize;
std::vector<uint8_t> sectorUsed_;
std::vector<uint32_t> liveInSector_;
std::vector<uint32_t> eraseMirror_;
struct FileMeta {
uint32_t dirId = 0;
std::string name;
bool isDir = false;
uint32_t inodeId = 0;
uint32_t dirIdSelf = 0;
uint32_t parentDirId = 0;
uint32_t size = 0;
uint32_t mtime = 0;
std::vector<uint8_t> data;
lolog::Loc loc{};
};
std::vector<FileMeta> byInode_;
/** dirId -> parentDirId (root parent is self). */
std::vector<uint32_t> dirParent_;
/** (dirId << 32 | hash) -> inode or dir id stored in name map via string key */
struct NameKey {
uint32_t dirId;
std::string name;
bool operator<(const NameKey &o) const;
};
struct NameVal {
bool isDir;
uint32_t id;
};
std::vector<std::pair<NameKey, NameVal>> nameIndex_;
struct PendingEnt {
uint8_t type;
std::vector<uint8_t> payload;
};
std::vector<PendingEnt> pendingGroup_;
std::vector<uint32_t> deadDirIds_;
std::vector<uint32_t> indexRunSeqs_;
uint32_t memtableBytes_ = 0;
uint32_t lastFlushMs_ = 0;
static constexpr uint32_t kIdleFlushMs = 30000;
uint32_t crc32(const uint8_t *data, size_t len) const;
bool readBytes(uint32_t addr, void *buf, size_t len);
bool progBytes(uint32_t addr, const void *buf, size_t len);
bool readSectorHeader(uint32_t sector, uint32_t &magic, uint32_t &epoch, uint32_t &seq, uint8_t &kind);
bool writeSectorHeader(uint32_t sector, uint32_t seq, uint8_t kind);
bool allocDataSector();
bool appendRaw(const uint8_t *payload, size_t payloadLen, uint8_t type, uint8_t flags);
void beginGroup();
bool endGroup();
void applyEntry(uint8_t type, const uint8_t *payload, size_t len);
void replaySector(uint32_t sector);
void rebuildIndexFromFlash();
bool resolvePath(const char *relPath, uint32_t &dirIdOut, std::string &nameOut, bool createDirs);
bool lookupName(uint32_t dirId, const char *name, NameVal &out) const;
void upsertName(uint32_t dirId, const char *name, const NameVal &val);
void eraseName(uint32_t dirId, const char *name);
FileMeta *metaByInode(uint32_t inodeId);
const FileMeta *metaByInode(uint32_t inodeId) const;
bool commitFileMeta(FileMeta &meta);
bool dirEmpty(uint32_t dirId) const;
void tombDir(uint32_t dirId);
bool hasRoomFor(uint32_t bytes) const;
uint32_t freeSectors() const;
void cleanOneSector();
bool allocIndexSector(uint32_t &sectorOut, uint32_t runSeq);
bool flushIndexRun();
void mergeIndexRunsIfNeeded();
bool loadNewestIndexRun();
void flushIndexIfNeeded();
bool copyLiveGroupsFromSector(uint32_t sector);
uint32_t nowMs() const;
};
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace lolog {
static constexpr uint32_t kSectorSize = 4096;
static constexpr uint32_t kPageSize = 256;
static constexpr uint32_t kHeaderSize = 16;
static constexpr uint32_t kInlineMax = 2048;
static constexpr uint32_t kMemtableBytes = 4096;
static constexpr uint32_t kMaxRuns = 4;
static constexpr uint32_t kHeadroomSectors = 2;
static constexpr uint32_t kMagicUsed = 0x4C4F4721; // "LOG!"
static constexpr uint32_t kMagicFree = 0xFFFFFFFF;
static constexpr uint16_t kEntryEnd = 0xFFFF;
static constexpr uint8_t kSegData = 0;
static constexpr uint8_t kSegIndex = 1;
static constexpr uint8_t kEntDir = 1;
static constexpr uint8_t kEntFile = 2;
static constexpr uint8_t kEntExtent = 3;
static constexpr uint8_t kEntTomb = 4;
static constexpr uint8_t kEntCommit = 5;
static constexpr uint8_t kFlagGroupEnd = 0x01;
static constexpr uint32_t kRootDirId = 0;
} // namespace lolog
+120
View File
@@ -0,0 +1,120 @@
#include "LoLogRamDevice.h"
#include "lofs/lolog/LoLogConfig.h"
#include <cstring>
LoLogRamDevice::LoLogRamDevice(uint32_t sectorCount) : sectors_(sectorCount)
{
data_.assign((size_t)sectors_ * lolog::kSectorSize, 0xFF);
}
bool LoLogRamDevice::begin()
{
return sectors_ > 0 && sectors_ <= kMaxSectors;
}
uint32_t LoLogRamDevice::sectorSize() const
{
return lolog::kSectorSize;
}
uint32_t LoLogRamDevice::pageSize() const
{
return lolog::kPageSize;
}
uint32_t LoLogRamDevice::sectorCount() const
{
return sectors_;
}
bool LoLogRamDevice::partialPageProgram() const
{
return true;
}
bool LoLogRamDevice::read(uint32_t addr, void *buf, size_t len)
{
if (!buf || addr + len > data_.size())
return false;
memcpy(buf, data_.data() + addr, len);
return true;
}
static bool canProgramByte(uint8_t oldB, uint8_t newB)
{
return (oldB & newB) == newB;
}
bool LoLogRamDevice::prog(uint32_t addr, const void *buf, size_t len)
{
if (!buf || addr + len > data_.size() || (len & 3) != 0 || (addr & 3) != 0)
return false;
progCallCount_++;
if (faultOnProgCall_ > 0 && progCallCount_ >= faultOnProgCall_)
return false;
const uint8_t *src = (const uint8_t *)buf;
for (size_t i = 0; i < len; i++) {
if (faultAfter_ > 0) {
if (progSinceFault_ >= faultAfter_)
return false;
progSinceFault_++;
}
uint8_t &cell = data_[addr + i];
if (!canProgramByte(cell, src[i]))
return false;
cell = src[i];
progBytes_++;
}
return true;
}
bool LoLogRamDevice::eraseSector(uint32_t sectorIndex)
{
if (sectorIndex >= sectors_)
return false;
memset(data_.data() + (size_t)sectorIndex * lolog::kSectorSize, 0xFF, lolog::kSectorSize);
if (sectorIndex < kMaxSectors)
eraseCounts_[sectorIndex]++;
return true;
}
bool LoLogRamDevice::sync()
{
return true;
}
void LoLogRamDevice::reset()
{
std::fill(data_.begin(), data_.end(), (uint8_t)0xFF);
memset(eraseCounts_, 0, sizeof(eraseCounts_));
faultAfter_ = 0;
progSinceFault_ = 0;
faultOnProgCall_ = 0;
progCallCount_ = 0;
progBytes_ = 0;
}
void LoLogRamDevice::setFaultAfterProgBytes(uint32_t n)
{
faultAfter_ = n;
progSinceFault_ = 0;
}
void LoLogRamDevice::setFaultOnProgCall(uint32_t n)
{
faultOnProgCall_ = n;
progCallCount_ = 0;
}
bool LoLogRamDevice::poke(uint32_t addr, uint8_t value)
{
if (addr >= data_.size())
return false;
uint8_t &cell = data_[addr];
if (!canProgramByte(cell, value))
return false;
cell = value;
return true;
}
#include "core/LoBBSStackGuard.h"
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "lofs/volumes/LoFSRawDevice.h"
#include <vector>
/** In-memory NOR model for host tests (1->0 program rules, fault injection). */
class LoLogRamDevice : public LoFSRawDevice {
public:
explicit LoLogRamDevice(uint32_t sectorCount = 64);
bool begin() override;
uint32_t sectorSize() const override;
uint32_t pageSize() const override;
uint32_t sectorCount() const override;
bool partialPageProgram() const override;
bool read(uint32_t addr, void *buf, size_t len) override;
bool prog(uint32_t addr, const void *buf, size_t len) override;
bool eraseSector(uint32_t sectorIndex) override;
bool sync() override;
void reset();
/** Fail the next program after this many bytes have been programmed (0 = off). */
void setFaultAfterProgBytes(uint32_t n);
uint32_t faultAfterProgBytes() const { return faultAfter_; }
/** Fail when starting prog call number n (1-based). 0 = off. */
void setFaultOnProgCall(uint32_t n);
uint32_t progCallCount() const { return progCallCount_; }
void clearProgCallCount() { progCallCount_ = 0; }
uint64_t progBytes() const { return progBytes_; }
void clearProgBytes() { progBytes_ = 0; }
/** Corrupt flash for torn-entry tests (must be 1->0 valid). */
bool poke(uint32_t addr, uint8_t value);
uint32_t sectorEraseCount(uint32_t sectorIndex) const { return eraseCount(sectorIndex); }
private:
std::vector<uint8_t> data_;
uint32_t sectors_;
uint32_t faultAfter_ = 0;
uint32_t progSinceFault_ = 0;
uint32_t faultOnProgCall_ = 0;
uint32_t progCallCount_ = 0;
uint64_t progBytes_ = 0;
};
+11 -25
View File
@@ -3,7 +3,7 @@
#include "LoFSMountsMeshcore.h"
#include "lofs/volumes/LoFSVolume.h"
#include "lofs/volumes/LoFSLittleFsVolume.h"
#include "lofs/volumes/LoFSNrfFlashBlockDevice.h"
#include "lofs/volumes/LoFSLoLogVolume.h"
#include "lofs/volumes/LoFSQspiNorBlockDevice.h"
#include "core/LoBBSConfig.h"
#if LOBBS_ARCH_ESP32
@@ -18,16 +18,10 @@
#if defined(LOFS_NRF52)
#include <Adafruit_LittleFS.h>
extern "C" uint8_t __flash3_start[] __attribute__((weak));
extern "C" uint8_t __flash3_end[] __attribute__((weak));
#if defined(EXTRAFS) && !defined(QSPIFLASH)
#include <CustomLFS.h>
extern CustomLFS ExtraFS;
#endif
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
extern CustomLFS_QSPIFlash QSPIFlash;
#endif
#endif
#if LOBBS_ARCH_ESP32
@@ -37,16 +31,13 @@ extern CustomLFS_QSPIFlash QSPIFlash;
namespace {
#if defined(LOFS_NRF52)
// Reserve window: same tuning as Meshtastic (128 KiB cap, lock host FS during raw flash, page erase).
LoFSNrfFlashBlockDevice gReserveDevice(__flash3_start, __flash3_end, 128, true, true, LOBBS_NRF52_FLASH_PAGE_SIZE);
LoFSLittleFsVolume gReserve(gReserveDevice);
LoFSLittleFsVolume gInternal;
LoFSLittleFsVolume gSpare;
#if defined(QSPIFLASH)
LoFSLittleFsVolume gQspi;
#else
LoFSQspiNorBlockDevice gQspiDevice;
LoFSLittleFsVolume gQspi(gQspiDevice);
#if defined(EXTRAFS) && !defined(QSPIFLASH)
LoFSLittleFsVolume gExtra;
#endif
#if LOFS_BOARD_HAS_QSPI
LoFSQspiNorBlockDevice gLofsDevice;
LoFSLoLogVolume gLofs(gLofsDevice);
#endif
#endif
@@ -85,17 +76,12 @@ void lofsMeshcoreSetHostFilesystem(void *filesystem)
void lofsPlatformMounts(LoFSMountTable &table)
{
#if defined(LOFS_NRF52)
if (__flash3_start && __flash3_end && (uintptr_t)__flash3_end > (uintptr_t)__flash3_start)
table.add({"reserve", &gReserve, false, true});
#if defined(QSPIFLASH)
gQspi.bind(QSPIFlash);
table.add({"qspi", &gQspi, false, true});
#elif LOFS_BOARD_HAS_QSPI
table.add({"qspi", &gQspi, false, true});
#if LOFS_BOARD_HAS_QSPI
table.add({"lofs", &gLofs, false, true});
#endif
#if defined(EXTRAFS) && !defined(QSPIFLASH)
gSpare.bind(ExtraFS);
table.add({"spare", &gSpare, true, true});
gExtra.bind(ExtraFS);
table.add({"extra", &gExtra, true, true});
#endif
table.add({"internal", &gInternal, true, false});
#elif LOBBS_ARCH_ESP32
@@ -6,8 +6,9 @@
#include "FSCommon.h"
#include "SPILock.h"
#include "lofs/volumes/LoFSLittleFsVolume.h"
#include "lofs/volumes/LoFSLoLogVolume.h"
#include "lofs/volumes/LoFSArduinoFsVolume.h"
#include "lofs/volumes/LoFSNrfFlashBlockDevice.h"
#include "lofs/volumes/LoFSNrfFlashRawDevice.h"
#include "lofs/volumes/LoFSQspiNorBlockDevice.h"
#include "lofs/volumes/LoFSSdVolume.h"
#include <cstring>
@@ -15,8 +16,6 @@
#if defined(LOFS_NRF52)
extern "C" uint8_t __flash2_start[] __attribute__((weak));
extern "C" uint8_t __flash2_end[] __attribute__((weak));
extern "C" uint8_t __flash3_start[] __attribute__((weak));
extern "C" uint8_t __flash3_end[] __attribute__((weak));
#include "flash/flash_nrf5x.h"
#endif
@@ -27,15 +26,14 @@ LoFSSdVolume gSd;
#endif
#if LOFS_BOARD_HAS_QSPI
LoFSQspiNorBlockDevice gQspiDevice;
LoFSLittleFsVolume gQspi(gQspiDevice);
LoFSQspiNorBlockDevice gLofsDevice;
LoFSLoLogVolume gLofs(gLofsDevice);
#elif defined(LOFS_NRF52)
LoFSNrfFlashRawDevice gLofsWindowDevice(__flash2_start, __flash2_end);
LoFSLoLogVolume gLofs(gLofsWindowDevice);
#endif
#if defined(LOFS_NRF52)
LoFSNrfFlashBlockDevice gReserveDevice(__flash3_start, __flash3_end, 128, true, true, LOBBS_NRF52_FLASH_PAGE_SIZE);
LoFSNrfFlashBlockDevice gSpareDevice(__flash2_start, __flash2_end, 128, true, true, LOBBS_NRF52_FLASH_PAGE_SIZE);
LoFSLittleFsVolume gReserve(gReserveDevice);
LoFSLittleFsVolume gSpare(gSpareDevice);
LoFSLittleFsVolume gInternal(FSCom);
#elif LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040
LoFSArduinoFsVolume<decltype(FSCom)> gInternal(FSCom);
@@ -48,6 +46,13 @@ void addSpec(LoFSMountTable &table, const char *name, LoFSVolume *vol, bool shar
table.add({name, vol, shared, formattable});
}
#if defined(LOFS_NRF52)
static bool flash2Present()
{
return (uintptr_t)__flash2_end > (uintptr_t)__flash2_start;
}
#endif
} // namespace
void lofsPlatformMounts(LoFSMountTable &table)
@@ -59,12 +64,10 @@ void lofsPlatformMounts(LoFSMountTable &table)
#endif
#if LOFS_BOARD_HAS_QSPI
addSpec(table, "qspi", &gQspi, false, true);
#endif
#if defined(LOFS_NRF52)
addSpec(table, "reserve", &gReserve, false, true);
addSpec(table, "spare", &gSpare, false, true);
addSpec(table, "lofs", &gLofs, false, true);
#elif defined(LOFS_NRF52)
if (flash2Present())
addSpec(table, "lofs", &gLofs, false, true);
#endif
addSpec(table, "internal", &gInternal, true, true);
+196
View File
@@ -0,0 +1,196 @@
#include "LoFSLoLogVolume.h"
#include <cstring>
#include <utility>
namespace {
struct LoLogFileHandle {
LoLog *log;
LoLog::OpenHandle *handle;
bool ownsHandle;
explicit LoLogFileHandle(LoLog *l, LoLog::OpenHandle *h, bool owns) : log(l), handle(h), ownsHandle(owns) {}
~LoLogFileHandle()
{
if (ownsHandle && log && handle)
log->closeHandle(handle);
}
};
void destroyLoLog(void *handle)
{
delete static_cast<LoLogFileHandle *>(handle);
}
bool validLoLog(void *handle)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->handle;
}
size_t readLoLog(void *handle, uint8_t *buf, size_t len)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->log && h->handle ? h->log->readHandle(h->handle, buf, len) : 0;
}
size_t writeLoLog(void *handle, const uint8_t *buf, size_t len)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->log && h->handle ? h->log->writeHandle(h->handle, buf, len) : 0;
}
bool seekLoLog(void *handle, uint32_t pos)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->log && h->handle && h->log->seekHandle(h->handle, pos);
}
uint32_t sizeLoLog(void *handle)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->log && h->handle ? h->log->sizeHandle(h->handle) : 0;
}
bool isDirLoLog(void *handle)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->log && h->handle && h->log->isDirHandle(h->handle);
}
const char *nameLoLog(void *handle)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
return h && h->log && h->handle ? h->log->nameHandle(h->handle) : "";
}
void closeLoLog(void *handle)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
if (h && h->ownsHandle && h->log && h->handle) {
h->log->closeHandle(h->handle);
h->handle = nullptr;
}
}
void flushLoLog(void *handle)
{
auto *h = static_cast<LoLogFileHandle *>(handle);
if (h && h->log && h->handle)
h->log->flushHandle(h->handle);
}
void *openNextLoLog(void *dirHandle)
{
auto *dir = static_cast<LoLogFileHandle *>(dirHandle);
if (!dir || !dir->log || !dir->handle || !dir->log->isDirHandle(dir->handle))
return nullptr;
LoLog::OpenHandle *next = dir->log->openNextInDir(dir->handle);
if (!next)
return nullptr;
return new LoLogFileHandle(dir->log, next, true);
}
} // namespace
const LoFileOps kLoFSLoLogFileOps = {
destroyLoLog, validLoLog, readLoLog, writeLoLog, seekLoLog, sizeLoLog,
isDirLoLog, nameLoLog, closeLoLog, flushLoLog, openNextLoLog,
};
LoFSLoLogVolume::LoFSLoLogVolume(LoFSRawDevice &device) : device_(device), log_(device) {}
uint8_t LoFSLoLogVolume::modeFromString(const char *mode)
{
if (!mode || strcmp(mode, "r") == 0)
return 0;
return 1;
}
LoFile LoFSLoLogVolume::wrapHandle(LoLog::OpenHandle *h, bool dirIter)
{
(void)dirIter;
if (!h)
return LoFile();
return LoFileAdopt(&kLoFSLoLogFileOps, new LoLogFileHandle(&log_, h, true));
}
bool LoFSLoLogVolume::begin()
{
ready_ = log_.mount();
return ready_;
}
void LoFSLoLogVolume::maintain(uint32_t budgetMs)
{
if (ready_)
log_.maintain(budgetMs);
}
LoFile LoFSLoLogVolume::open(const char *relPath, const char *mode)
{
if (!ready_)
return LoFile();
const char *rel = normalizeRel(relPath);
const bool write = modeFromString(mode) != 0;
if (strcmp(mode, "r") == 0 && log_.isDirectory(rel)) {
LoLog::OpenHandle *d = log_.openDir(rel);
return wrapHandle(d, true);
}
LoLog::OpenHandle *h = log_.open(rel, write, write);
return wrapHandle(h, false);
}
LoFile LoFSLoLogVolume::open(const char *relPath, uint8_t mode)
{
return open(relPath, mode ? "w" : "r");
}
bool LoFSLoLogVolume::exists(const char *relPath)
{
return ready_ && log_.exists(normalizeRel(relPath));
}
bool LoFSLoLogVolume::mkdir(const char *relPath)
{
return ready_ && log_.mkdir(normalizeRel(relPath));
}
bool LoFSLoLogVolume::remove(const char *relPath)
{
return ready_ && log_.remove(normalizeRel(relPath));
}
bool LoFSLoLogVolume::rename(const char *oldPath, const char *newPath)
{
return ready_ && log_.rename(normalizeRel(oldPath), normalizeRel(newPath));
}
bool LoFSLoLogVolume::rmdir(const char *relPath)
{
return ready_ && log_.rmdir(normalizeRel(relPath), true);
}
uint64_t LoFSLoLogVolume::totalBytes()
{
return ready_ ? log_.totalBytes() : 0;
}
uint64_t LoFSLoLogVolume::usedBytes()
{
return ready_ ? log_.usedBytes() : 0;
}
bool LoFSLoLogVolume::format()
{
if (!device_.begin())
return false;
ready_ = log_.format();
return ready_;
}
void LoFSLoLogVolume::spaceHint(uint32_t *blockOut, uint32_t *slackOut)
{
log_.spaceHint(blockOut, slackOut);
}
#include "core/LoBBSStackGuard.h"
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "LoFSRawDevice.h"
#include "LoFSVolume.h"
#include "lofs/lolog/LoLog.h"
class LoFSLoLogVolume : public LoFSVolume {
public:
explicit LoFSLoLogVolume(LoFSRawDevice &device);
bool begin() override;
void maintain(uint32_t budgetMs) override;
LoFile open(const char *relPath, const char *mode) override;
LoFile open(const char *relPath, uint8_t mode) override;
bool exists(const char *relPath) override;
bool mkdir(const char *relPath) override;
bool remove(const char *relPath) override;
bool rename(const char *oldPath, const char *newPath) override;
bool rmdir(const char *relPath) override;
uint64_t totalBytes() override;
uint64_t usedBytes() override;
bool format() override;
void spaceHint(uint32_t *blockOut, uint32_t *slackOut) override;
private:
LoFSRawDevice &device_;
LoLog log_;
bool ready_ = false;
static uint8_t modeFromString(const char *mode);
LoFile wrapHandle(LoLog::OpenHandle *h, bool dirIter);
};
extern const LoFileOps kLoFSLoLogFileOps;
+166
View File
@@ -0,0 +1,166 @@
#include "LoFSNrfFlashRawDevice.h"
#include "lofs/lolog/LoLogConfig.h"
#include <cstring>
#if defined(LOFS_NRF52)
#if defined(LOBBS_PLATFORM_MESHTASTIC)
#include "flash/flash_nrf5x.h"
#include <InternalFileSystem.h>
#elif defined(LOBBS_PLATFORM_MESHCORE)
extern "C" int flash_nrf5x_read(void *buffer, uint32_t address, uint32_t len);
extern "C" int flash_nrf5x_write(uint32_t address, const void *data, uint32_t len);
extern "C" void flash_nrf5x_flush(void);
extern "C" void flash_nrf5x_erase(uint32_t address);
#endif
LoFSNrfFlashRawDevice::LoFSNrfFlashRawDevice(void *startSym, void *endSym, bool lockWithInternalFs,
uint32_t flashPageSize)
: startSym_(startSym), endSym_(endSym), lockWithInternalFs_(lockWithInternalFs), flashPageSize_(flashPageSize)
{
}
void LoFSNrfFlashRawDevice::lockFs()
{
#if defined(LOBBS_PLATFORM_MESHTASTIC)
if (lockWithInternalFs_)
InternalFS._lockFS();
#else
(void)lockWithInternalFs_;
#endif
}
void LoFSNrfFlashRawDevice::unlockFs()
{
#if defined(LOBBS_PLATFORM_MESHTASTIC)
if (lockWithInternalFs_)
InternalFS._unlockFS();
#endif
}
bool LoFSNrfFlashRawDevice::begin()
{
const uint32_t start = (uint32_t)(uintptr_t)startSym_;
const uint32_t end = (uint32_t)(uintptr_t)endSym_;
if (!start || end <= start)
return false;
base_ = start;
size_ = end - start;
if (size_ < lolog::kSectorSize)
return false;
return true;
}
uint32_t LoFSNrfFlashRawDevice::sectorSize() const
{
return lolog::kSectorSize;
}
uint32_t LoFSNrfFlashRawDevice::pageSize() const
{
return lolog::kPageSize;
}
uint32_t LoFSNrfFlashRawDevice::sectorCount() const
{
return size_ / lolog::kSectorSize;
}
bool LoFSNrfFlashRawDevice::partialPageProgram() const
{
return true;
}
bool LoFSNrfFlashRawDevice::read(uint32_t addr, void *buf, size_t len)
{
if (!buf || addr + len > size_)
return false;
lockFs();
int n = flash_nrf5x_read(buf, base_ + addr, (uint32_t)len);
unlockFs();
return n > 0;
}
bool LoFSNrfFlashRawDevice::prog(uint32_t addr, const void *buf, size_t len)
{
if (!buf || addr + len > size_ || (len & 3) != 0 || (addr & 3) != 0)
return false;
lockFs();
int n = flash_nrf5x_write(base_ + addr, buf, (uint32_t)len);
unlockFs();
return n > 0;
}
bool LoFSNrfFlashRawDevice::eraseSector(uint32_t sectorIndex)
{
if (sectorIndex >= sectorCount())
return false;
const uint32_t addr = base_ + sectorIndex * lolog::kSectorSize;
lockFs();
flash_nrf5x_erase(addr);
unlockFs();
if (sectorIndex < kMaxSectors)
eraseCounts_[sectorIndex]++;
return true;
}
bool LoFSNrfFlashRawDevice::sync()
{
lockFs();
flash_nrf5x_flush();
unlockFs();
return true;
}
#else
LoFSNrfFlashRawDevice::LoFSNrfFlashRawDevice(void *, void *, bool, uint32_t) {}
bool LoFSNrfFlashRawDevice::begin()
{
return false;
}
uint32_t LoFSNrfFlashRawDevice::sectorSize() const
{
return 4096;
}
uint32_t LoFSNrfFlashRawDevice::pageSize() const
{
return 256;
}
uint32_t LoFSNrfFlashRawDevice::sectorCount() const
{
return 0;
}
bool LoFSNrfFlashRawDevice::partialPageProgram() const
{
return true;
}
bool LoFSNrfFlashRawDevice::read(uint32_t, void *, size_t)
{
return false;
}
bool LoFSNrfFlashRawDevice::prog(uint32_t, const void *, size_t)
{
return false;
}
bool LoFSNrfFlashRawDevice::eraseSector(uint32_t)
{
return false;
}
bool LoFSNrfFlashRawDevice::sync()
{
return true;
}
#endif
#include "core/LoBBSStackGuard.h"
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "LoFSRawDevice.h"
/** nRF52840 internal flash window for LoLog (not LittleFS). */
class LoFSNrfFlashRawDevice : public LoFSRawDevice {
public:
LoFSNrfFlashRawDevice(void *startSym, void *endSym, bool lockWithInternalFs = true,
uint32_t flashPageSize = 4096);
bool begin() override;
uint32_t sectorSize() const override;
uint32_t pageSize() const override;
uint32_t sectorCount() const override;
bool partialPageProgram() const override;
bool read(uint32_t addr, void *buf, size_t len) override;
bool prog(uint32_t addr, const void *buf, size_t len) override;
bool eraseSector(uint32_t sectorIndex) override;
bool sync() override;
private:
void *startSym_;
void *endSym_;
bool lockWithInternalFs_;
uint32_t flashPageSize_;
uint32_t base_ = 0;
uint32_t size_ = 0;
void lockFs();
void unlockFs();
};
+104 -45
View File
@@ -1,19 +1,17 @@
#include "LoFSQspiNorBlockDevice.h"
#include <Arduino.h>
#include <cstring>
#if LOFS_BOARD_HAS_QSPI
#include "variant.h"
#include "nrf.h"
#include <cstring>
#include <nrfx_qspi.h>
extern const uint32_t g_ADigitalPinMap[];
#include <Adafruit_LittleFS.h>
static constexpr uint32_t LOFS_QSPI_BLOCK = 4096;
static constexpr uint32_t LOFS_QSPI_SECTOR = 4096;
static constexpr uint32_t LOFS_QSPI_PAGE = 256;
static constexpr uint32_t LOFS_QSPI_BLOCK_COUNT = 512;
static constexpr uint32_t LOFS_QSPI_SECTOR_COUNT = 512;
static uint8_t lofsQspiScratch[LOFS_QSPI_PAGE] __attribute__((aligned(4)));
static bool lofsQspiHwReady = false;
@@ -97,98 +95,159 @@ static bool lofsQspiInitHw()
return true;
}
static int lofsQspiRead(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size)
static bool lofsQspiReadAddr(uint32_t addr, void *buffer, size_t size)
{
(void)c;
uint32_t addr = block * LOFS_QSPI_BLOCK + off;
if (((uintptr_t)buffer & 3) == 0 && (size & 3) == 0) {
nrfx_err_t err = nrfx_qspi_read(buffer, size, addr);
return err == NRFX_SUCCESS ? 0 : -1;
return err == NRFX_SUCCESS;
}
uint8_t *dst = (uint8_t *)buffer;
while (size > 0) {
uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : size;
uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : (uint32_t)size;
uint32_t qchunk = (chunk + 3) & ~3u;
nrfx_err_t err = nrfx_qspi_read(lofsQspiScratch, qchunk, addr);
if (err != NRFX_SUCCESS)
return -1;
return false;
memcpy(dst, lofsQspiScratch, chunk);
dst += chunk;
addr += chunk;
size -= chunk;
}
return 0;
return true;
}
static int lofsQspiProg(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer,
lfs_size_t size)
static bool lofsQspiProgAddr(uint32_t addr, const void *buffer, size_t size)
{
(void)c;
uint32_t addr = block * LOFS_QSPI_BLOCK + off;
if (((uintptr_t)buffer & 3) == 0 && (size & 3) == 0) {
nrfx_err_t err = nrfx_qspi_write(buffer, size, addr);
lofsQspiWaitReady();
return err == NRFX_SUCCESS ? 0 : -1;
return err == NRFX_SUCCESS;
}
const uint8_t *src = (const uint8_t *)buffer;
while (size > 0) {
uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : size;
uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : (uint32_t)size;
uint32_t qchunk = (chunk + 3) & ~3u;
memcpy(lofsQspiScratch, src, chunk);
for (uint32_t i = chunk; i < qchunk; i++)
lofsQspiScratch[i] = 0xFF;
nrfx_err_t err = nrfx_qspi_write(lofsQspiScratch, qchunk, addr);
if (err != NRFX_SUCCESS)
return -1;
return false;
src += chunk;
addr += chunk;
size -= chunk;
}
lofsQspiWaitReady();
return 0;
return true;
}
static int lofsQspiErase(const struct lfs_config *c, lfs_block_t block)
bool LoFSQspiNorBlockDevice::begin()
{
(void)c;
uint32_t addr = block * LOFS_QSPI_BLOCK;
return lofsQspiInitHw();
}
uint32_t LoFSQspiNorBlockDevice::sectorSize() const
{
return LOFS_QSPI_SECTOR;
}
uint32_t LoFSQspiNorBlockDevice::pageSize() const
{
return LOFS_QSPI_PAGE;
}
uint32_t LoFSQspiNorBlockDevice::sectorCount() const
{
return LOFS_QSPI_SECTOR_COUNT;
}
bool LoFSQspiNorBlockDevice::partialPageProgram() const
{
return true;
}
bool LoFSQspiNorBlockDevice::read(uint32_t addr, void *buf, size_t len)
{
return lofsQspiReadAddr(addr, buf, len);
}
bool LoFSQspiNorBlockDevice::prog(uint32_t addr, const void *buf, size_t len)
{
return lofsQspiProgAddr(addr, buf, len);
}
bool LoFSQspiNorBlockDevice::eraseSector(uint32_t sectorIndex)
{
if (sectorIndex >= LOFS_QSPI_SECTOR_COUNT)
return false;
uint32_t addr = sectorIndex * LOFS_QSPI_SECTOR;
nrfx_err_t err = nrfx_qspi_erase(NRF_QSPI_ERASE_LEN_4KB, addr);
lofsQspiWaitReady();
return err == NRFX_SUCCESS ? 0 : -1;
if (err == NRFX_SUCCESS && sectorIndex < kMaxSectors)
eraseCounts_[sectorIndex]++;
return err == NRFX_SUCCESS;
}
static int lofsQspiSync(const struct lfs_config *c)
bool LoFSQspiNorBlockDevice::sync()
{
(void)c;
lofsQspiWaitReady();
return 0;
}
bool LoFSQspiNorBlockDevice::fill(lfs_config &cfg)
{
if (!lofsQspiInitHw())
return false;
cfg.context = NULL;
cfg.read = lofsQspiRead;
cfg.prog = lofsQspiProg;
cfg.erase = lofsQspiErase;
cfg.sync = lofsQspiSync;
cfg.read_size = LOFS_QSPI_PAGE;
cfg.prog_size = LOFS_QSPI_PAGE;
cfg.block_size = LOFS_QSPI_BLOCK;
cfg.block_count = LOFS_QSPI_BLOCK_COUNT;
cfg.lookahead = 512;
return true;
}
#else
bool LoFSQspiNorBlockDevice::fill(lfs_config &cfg)
bool LoFSQspiNorBlockDevice::begin()
{
(void)cfg;
return false;
}
uint32_t LoFSQspiNorBlockDevice::sectorSize() const
{
return 4096;
}
uint32_t LoFSQspiNorBlockDevice::pageSize() const
{
return 256;
}
uint32_t LoFSQspiNorBlockDevice::sectorCount() const
{
return 0;
}
bool LoFSQspiNorBlockDevice::partialPageProgram() const
{
return true;
}
bool LoFSQspiNorBlockDevice::read(uint32_t addr, void *buf, size_t len)
{
(void)addr;
(void)buf;
(void)len;
return false;
}
bool LoFSQspiNorBlockDevice::prog(uint32_t addr, const void *buf, size_t len)
{
(void)addr;
(void)buf;
(void)len;
return false;
}
bool LoFSQspiNorBlockDevice::eraseSector(uint32_t sectorIndex)
{
(void)sectorIndex;
return false;
}
bool LoFSQspiNorBlockDevice::sync()
{
return true;
}
#endif
#include "core/LoBBSStackGuard.h"
+12 -3
View File
@@ -1,7 +1,7 @@
#pragma once
#include "core/LoBBSConfig.h"
#include "LoFSBlockDevice.h"
#include "LoFSRawDevice.h"
#if !defined(LOFS_BOARD_HAS_QSPI)
#if ((defined(LOFS_ENABLE_EXTRA_QSPI) && LOFS_ENABLE_EXTRA_QSPI) || defined(QSPIFLASH)) && \
@@ -12,7 +12,16 @@
#endif
#endif
class LoFSQspiNorBlockDevice : public LoFSBlockDevice {
class LoFSQspiNorBlockDevice : public LoFSRawDevice {
public:
bool fill(lfs_config &cfg) override;
bool begin() override;
uint32_t sectorSize() const override;
uint32_t pageSize() const override;
uint32_t sectorCount() const override;
bool partialPageProgram() const override;
bool read(uint32_t addr, void *buf, size_t len) override;
bool prog(uint32_t addr, const void *buf, size_t len) override;
bool eraseSector(uint32_t sectorIndex) override;
bool sync() override;
};
+8
View File
@@ -0,0 +1,8 @@
#include "LoFSRawDevice.h"
uint32_t LoFSRawDevice::eraseCount(uint32_t sectorIndex) const
{
return sectorIndex < kMaxSectors ? eraseCounts_[sectorIndex] : 0;
}
#include "core/LoBBSStackGuard.h"
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
/** Raw NOR or RAM backing for LoLog (not LittleFS). */
class LoFSRawDevice {
public:
virtual ~LoFSRawDevice() = default;
virtual bool begin() = 0;
virtual uint32_t sectorSize() const = 0;
virtual uint32_t pageSize() const = 0;
virtual uint32_t sectorCount() const = 0;
/** Multiple programs into the same erased page before erase. */
virtual bool partialPageProgram() const = 0;
virtual bool read(uint32_t addr, void *buf, size_t len) = 0;
/** len 4-byte aligned; addr page-aligned offset within device. */
virtual bool prog(uint32_t addr, const void *buf, size_t len) = 0;
virtual bool eraseSector(uint32_t sectorIndex) = 0;
virtual bool sync() = 0;
uint32_t eraseCount(uint32_t sectorIndex) const;
protected:
static constexpr uint32_t kMaxSectors = 2048;
uint32_t eraseCounts_[kMaxSectors] = {};
};
+5 -2
View File
@@ -4,8 +4,8 @@
#include <stddef.h>
#include <stdint.h>
static constexpr const char *LOFS_MOUNT_ORDER[] = {"sd", "qspi", "reserve", "spare", "internal"};
static constexpr int LOFS_MOUNT_ORDER_LEN = 5;
static constexpr const char *LOFS_MOUNT_ORDER[] = {"sd", "lofs", "extra", "internal"};
static constexpr int LOFS_MOUNT_ORDER_LEN = 4;
static constexpr int LOFS_MAX_MOUNTS = 6;
class LoFSVolume {
@@ -29,6 +29,9 @@ class LoFSVolume {
virtual bool format() = 0;
virtual void spaceHint(uint32_t *blockOut, uint32_t *slackOut) = 0;
/** Background flush, merge, and cleaning (LoLog volumes). */
virtual void maintain(uint32_t budgetMs) { (void)budgetMs; }
static const char *normalizeRel(const char *rel);
};
@@ -19,6 +19,7 @@
#include "platforms/arduino/LoPlatformArduino.h"
#include "core/LoBBSDispatch.h"
#include "core/LoBBSKernel.h"
#include <lofs/LoFS.h>
#include "platforms/meshcore/LoPlatformMeshcore.h"
#include "lofs/platforms/meshcore/LoFSMountsMeshcore.h"
@@ -165,6 +166,7 @@ void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, void *filesystem,
void lobbsMeshCoreLoop(LoBBSKernel *core)
{
(void)core;
LoFS::maintain(15);
}
static void lobbsFillInbound(LoInbound &in, const ContactInfo &from, const char *text)
+8
View File
@@ -1,6 +1,8 @@
#if defined(LOBBS_PLATFORM_MESHTASTIC)
#include "platforms/meshtastic/LoBBSModule.h"
#include "platforms/meshtastic/LoBBSDispatchMeshtastic.h"
#include "platforms/LoPlatform.h"
#include <lofs/LoFS.h>
LoBBSModule::LoBBSModule() : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_MESSAGE_APP)
{
@@ -9,6 +11,12 @@ LoBBSModule::LoBBSModule() : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_M
ProcessMessage LoBBSModule::handleReceived(const meshtastic_MeshPacket &mp)
{
static uint32_t lastMaintainMs = 0;
const uint32_t now = lobbsPlatformMillis();
if (now - lastMaintainMs >= 5000) {
lastMaintainMs = now;
LoFS::maintain(15);
}
return lobbsDispatchReceived(&kernel_, mp);
}
#endif