diff --git a/.cursor/rules/lobbs-project.mdc b/.cursor/rules/lobbs-project.mdc index 207d1b4..2a7e5c8 100644 --- a/.cursor/rules/lobbs-project.mdc +++ b/.cursor/rules/lobbs-project.mdc @@ -11,7 +11,7 @@ Cross-platform, on-firmware BBS for **Meshtastic** and **MeshCore**. Greenfield: **Plugin owns everything, forks stay upstream.** -1. **Plugin** (this repo, PlatformIO library): core, apps, LoScalar, LoDB, LoFS, protocol, and the platform providers under `platforms//`. On MeshCore the plugin also owns the firmware app (`setup()`/`loop()`). +1. **Plugin** (this repo, PlatformIO library): `core/` (kernel), apps, LoScalar, LoDB, LoFS, protocol, and platform providers under `platforms/` (`meshtastic`, `meshcore`, `native` for CI only). Product platforms are Meshtastic and MeshCore only. On MeshCore the plugin owns the BBS integration surface (`lobbsMeshCoreInit`, DM helpers); the fork wires example chat. 2. **Firmware forks** (`../lobbs-meshtastic-firmware`, `../lobbs-meshcore-firmware`, both on `lobbs`): upstream plus a thin overlay that pins the plugin in `lib_deps`. The Meshtastic fork enables LoBBS on every env. The repos are siblings under `/Volumes/Code/repos/meshenvy/`. No submodules anywhere: PlatformIO clones git deps with `--recursive`, and MeshForge builds a fork repo alone. diff --git a/.gitignore b/.gitignore index e43b0f9..981bceb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +.pio/ +__pycache__/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3339fbc..60c8ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 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. + ### Added - Stateless slash CLI: `/[id] command args…`, replies optionally prefixed ``. No menu stack. The last successful reply is cached for `/pN` paging. diff --git a/LoBBSBootTrace.h b/LoBBSBootTrace.h deleted file mode 100644 index f37c2d0..0000000 --- a/LoBBSBootTrace.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once -#include "configuration.h" - -/** Log immediately before a boot step; last line wins when diagnosing hangs. */ -#define LOBBS_BOOT_STEP(msg) LOG_INFO("LoBBS> %s", (msg)) - diff --git a/LoBBSDispatch.cpp b/LoBBSDispatch.cpp deleted file mode 100644 index aea3662..0000000 --- a/LoBBSDispatch.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "LoBBSDispatch.h" -#include "LoBBSCommandRegistry.h" -#include "LoBBSModule.h" -#include "apps/Auth/AuthDal.h" -#include "mesh/MeshTypes.h" -#include "mesh/NodeDB.h" -#include - -#include "LoBBSStackGuard.h" - -static void lobbsTrimLine(char *line) -{ - if (!line) - return; - size_t len = strlen(line); - while (len > 0 && (line[len - 1] == ' ' || line[len - 1] == '\t' || line[len - 1] == '\r' || line[len - 1] == '\n')) - line[--len] = '\0'; - char *start = line; - while (*start == ' ' || *start == '\t') - start++; - if (start != line) - memmove(line, start, strlen(start) + 1); -} - -static LoBBSSession lobbsResolveSession(LoBBSModule *mod, uint32_t wireNodeId) -{ - LoBBSSession session; - session.nodeId = wireNodeId; - AuthDal &auth = mod->auth().dal(); - LoScalar userRow; - uint32_t sessionNodeId = wireNodeId; - uint64_t authUserUuid = 0; - std::string cwd; - if (!auth.loadUserByNodeId(wireNodeId, &userRow, &sessionNodeId, &authUserUuid, &cwd)) - return session; - session.nodeId = sessionNodeId; - session.userUuid = authUserUuid; - if (!cwd.empty() && cwd[0] == '/' && cwd.size() < sizeof(session.cwd)) - memcpy(session.cwd, cwd.c_str(), cwd.size() + 1); - if (!AuthDal::userUsername(userRow, session.username, sizeof(session.username))) - session.username[0] = '\0'; - session.isSysop = AuthDal::userIsSysop(userRow); - return session; -} - -ProcessMessage lobbsDispatchReceived(LoBBSModule *mod, const meshtastic_MeshPacket &mp) -{ - if (!isToUs(&mp)) - return ProcessMessage::CONTINUE; - - if (mp.decoded.payload.size == 0) - return ProcessMessage::CONTINUE; - - size_t copyLen = mp.decoded.payload.size; - if (copyLen >= sizeof(mod->msgBuffer)) - copyLen = sizeof(mod->msgBuffer) - 1; - memcpy(mod->msgBuffer, mp.decoded.payload.bytes, copyLen); - mod->msgBuffer[copyLen] = '\0'; - - lobbsTrimLine(mod->msgBuffer); - if (mod->msgBuffer[0] == '\0' || mod->msgBuffer[0] != '/') - return ProcessMessage::CONTINUE; - - const LoBBSSession session = lobbsResolveSession(mod, getFrom(&mp)); - lobbsCommandsHandle(mod, mp, session, mod->msgBuffer); -#ifdef ARCH_NRF52 - // The loop stack has no overflow trap; warn once while there is still room to see the log. - static bool lowStackWarned = false; - unsigned headroom = (unsigned)(uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t)); - if (!lowStackWarned && headroom < 1024) { - lowStackWarned = true; - LOG_WARN("LoBBS loop stack headroom low: %u B", headroom); - } -#endif - return ProcessMessage::CONTINUE; -} - diff --git a/LoBBSDispatch.h b/LoBBSDispatch.h deleted file mode 100644 index 4ad056e..0000000 --- a/LoBBSDispatch.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include "MeshModule.h" -#include "mesh/generated/meshtastic/mesh.pb.h" - -class LoBBSModule; - -ProcessMessage lobbsDispatchReceived(LoBBSModule *mod, const meshtastic_MeshPacket &mp); - diff --git a/LoBBSModule.cpp b/LoBBSModule.cpp deleted file mode 100644 index ecbce8c..0000000 --- a/LoBBSModule.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include "LoBBSModule.h" -#include "LoBBSConfig.h" -#include "LoBBSDispatch.h" -#include "LoBBSInstall.h" -#include "LoBBSWireup.h" -#include -#if LOBBS_SEED -#include "LoBBSSeed.h" -#endif -#include "LoBBSReply.h" -#include "MeshService.h" -#include -#include - -#include "LoBBSBootTrace.h" -#include "LoBBSStackGuard.h" - -LoBBSModule::LoBBSModule() - : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_MESSAGE_APP), lodb_(new LoDb("db")), config_(*lodb_), auth_(*lodb_), - mail_(*lodb_), news_(*lodb_), yarn_(*lodb_), wall_(*lodb_) -{ - LOBBS_BOOT_STEP("module ctor body"); - LOBBS_BOOT_STEP("LoFS::begin"); - LoFS::begin(); - LOBBS_BOOT_STEP("lobbsWireup"); - lobbsWireup(); -#ifdef LOBBS_DEMO_MODE - LOBBS_BOOT_STEP("lobbsInstallAutoSeed"); - lobbsInstallAutoSeed(*this); - LOBBS_BOOT_STEP("lobbsSeedAll"); - lobbsSeedAll(*this); -#else - LOBBS_BOOT_STEP("lobbsInstallInit"); - lobbsInstallInit(*this); -#endif - LOBBS_BOOT_STEP("module ctor done"); -} - -LoBBSInstallState LoBBSModule::installState() const -{ - return lobbsInstallState(*this); -} - -LoBBSModule::~LoBBSModule() -{ - delete lodb_; -} - -ProcessMessage LoBBSModule::handleReceived(const meshtastic_MeshPacket &mp) -{ - return lobbsDispatchReceived(this, mp); -} - -void lobbsBreadcrumb(const char *step) -{ - if (!step) - return; -#if !defined(ARCH_PORTDUINO) - Serial.print("INFO | LoBBS "); - Serial.println(step); -#else - LOG_INFO("LoBBS %s", step); -#endif -} - -#ifdef PIO_UNIT_TESTING -#include -std::vector *lobbsTestReplySink = nullptr; -#endif - -void LoBBSModule::sendReply(const meshtastic_MeshPacket &req, const char *msg) -{ - if (!msg) - return; -#ifdef PIO_UNIT_TESTING - if (lobbsTestReplySink) { - lobbsTestReplySink->push_back(msg); - return; - } -#endif - char mark[40]; - snprintf(mark, sizeof(mark), "reply alloc %uB", (unsigned)strlen(msg)); - lobbsBreadcrumb(mark); - meshtastic_MeshPacket *reply = allocDataPacket(); - if (!reply) { - lobbsBreadcrumb("reply alloc failed"); - LOG_WARN("LoBBS: no packet for reply (pool exhausted)"); - return; - } - static constexpr char truncMarker[] = "[...]"; - static constexpr size_t truncMarkerLen = sizeof(truncMarker) - 1; - - size_t msgLen = strlen(msg); - bool isTruncated = msgLen > LOBBS_REPLY_BYTES; - size_t payloadSize = isTruncated ? LOBBS_REPLY_BYTES : msgLen; - reply->decoded.payload.size = payloadSize; - - if (isTruncated) { - size_t copyLen = LOBBS_REPLY_BYTES > truncMarkerLen ? LOBBS_REPLY_BYTES - truncMarkerLen : 0; - memcpy(reply->decoded.payload.bytes, msg, copyLen); - memcpy(reply->decoded.payload.bytes + copyLen, truncMarker, truncMarkerLen); - } else { - memcpy(reply->decoded.payload.bytes, msg, payloadSize); - } - setReplyTo(reply, req); - reply->decoded.want_response = false; - snprintf(mark, sizeof(mark), "reply send %08x", reply->id); - lobbsBreadcrumb(mark); - // Phone ToRadio uses from=0; RoutingModule only forwards to the app when from != 0. - const bool ccPhone = (req.from == 0); - service->sendToMesh(reply, RX_SRC_LOCAL, ccPhone); - lobbsBreadcrumb("reply out"); -} - diff --git a/LoBBSSeed.h b/LoBBSSeed.h deleted file mode 100644 index 8e9ecbf..0000000 --- a/LoBBSSeed.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#if LOBBS_SEED - -class LoBBSModule; -void lobbsSeedAll(LoBBSModule &mod); - -#endif diff --git a/README.md b/README.md index 56e3a58..2fa2d2b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ On-firmware bulletin board for **Meshtastic** and **MeshCore**. This repo is the | [MeshEnvy/lobbs-meshtastic-firmware](https://github.com/MeshEnvy/lobbs-meshtastic-firmware) | `lobbs` | | [MeshEnvy/lobbs-meshcore-firmware](https://github.com/MeshEnvy/lobbs-meshcore-firmware) | `lobbs` | -Each fork builds standalone. Its `variants/lobbs/` config pins this plugin by commit in `lib_deps`, so cloning one fork is enough to build it. +Each fork builds standalone. Meshtastic pulls the plugin via `lobbs-overrides.ini` on every env; MeshCore adds `LOBBS_PLATFORM_MESHCORE` and optional example wiring. Committed pins use `lib_deps` by commit hash. ## Local development @@ -29,8 +29,24 @@ The plugin is tagged `vX.Y.Z`. Each fork release is tagged `lobbs-vX.Y.Z. #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" void lobbsAppCopyCapped(char *dst, size_t dstCap, const char *src, size_t srcCap) { @@ -27,7 +27,7 @@ void lobbsAppCopyCapped(char *dst, size_t dstCap, const char *src, size_t srcCap void lobbsAppTimeAgo(uint32_t timestamp, char *buffer, size_t bufferSize) { - uint32_t now = getTime(); + uint32_t now = lobbsPlatformUnixNow(); if (now < timestamp) { snprintf(buffer, bufferSize, "now"); return; @@ -56,33 +56,33 @@ void lobbsAppTruncMsg(const char *message, char *buffer, size_t bufferSize, size } } -bool lobbsAppLoadUser(LoBBSModule *mod, uint64_t uuid, LoScalar *outUser) +bool lobbsAppLoadUser(LoBBSKernel *kernel, uint64_t uuid, LoScalar *outUser) { - if (!mod || !outUser || uuid == 0) + if (!kernel || !outUser || uuid == 0) return false; - return mod->auth().dal().loadUserByUuid(uuid, outUser); + return kernel->auth().dal().loadUserByUuid(uuid, outUser); } -void lobbsAppUsernameForUuid(LoBBSModule *mod, uint64_t uuid, char *buf, size_t bufCap) +void lobbsAppUsernameForUuid(LoBBSKernel *kernel, uint64_t uuid, char *buf, size_t bufCap) { if (!buf || bufCap == 0) return; buf[0] = '\0'; LoScalar user; - if (!lobbsAppLoadUser(mod, uuid, &user) || !AuthDal::userUsername(user, buf, bufCap) || !buf[0]) + if (!lobbsAppLoadUser(kernel, uuid, &user) || !AuthDal::userUsername(user, buf, bufCap) || !buf[0]) lobbsAppCopyCapped(buf, bufCap, "unknown", 7); } -uint64_t lobbsAppUuidForUsername(LoBBSModule *mod, const char *username) +uint64_t lobbsAppUuidForUsername(LoBBSKernel *kernel, const char *username) { - if (!mod || !username || !username[0]) + if (!kernel || !username || !username[0]) return 0; - return mod->auth().dal().getUserUuidByUsername(username); + return kernel->auth().dal().getUserUuidByUsername(username); } bool lobbsAppResolveUsername(LoBBSCommandCtx &ctx, const char *username, uint64_t &uuidOut) { - uuidOut = lobbsAppUuidForUsername(ctx.mod, username); + uuidOut = lobbsAppUuidForUsername(ctx.kernel, username); if (uuidOut != 0) return true; char msg[64]; @@ -100,7 +100,7 @@ static uint32_t quotaLoad(LoDb &db, const char *table, uint64_t userUuid, uint32 uint32_t start = 0; if (db.get(table, userUuid, rec) != LODB_OK || !rec.getUint32(LoBBSQuotaField::FIELD_CYCLE_START, start) || start == 0) return 0; - if (getTime() >= start + periodSec) + if (lobbsPlatformUnixNow() >= start + periodSec) return 0; for (size_t i = 0; i < n; i++) rec.getUint32(LoBBSQuotaField::FIELD_USED + (uint32_t)i, used[i]); @@ -139,7 +139,7 @@ LoDbError lobbsQuotaAdd(LoDb &db, const char *table, uint64_t userUuid, uint32_t uint32_t used[LOBBS_QUOTA_MAX_COUNTERS]; uint32_t start = quotaLoad(db, table, userUuid, periodSec, used, n); if (start == 0) - start = getTime(); + start = lobbsPlatformUnixNow(); LoScalar rec; rec.setUint64(LoBBSQuotaField::FIELD_USER_UUID, userUuid); rec.setUint32(LoBBSQuotaField::FIELD_CYCLE_START, start); diff --git a/apps/AppUtil.h b/apps/AppUtil.h index b0a78f3..dcd5aed 100644 --- a/apps/AppUtil.h +++ b/apps/AppUtil.h @@ -4,7 +4,7 @@ #include #include -class LoBBSModule; +class LoBBSKernel; struct LoBBSCommandCtx; /** Reply text for a failed write: "Disk full." for LODB_ERR_FULL, else `otherwise`. */ @@ -14,10 +14,10 @@ void lobbsAppCopyCapped(char *dst, size_t dstCap, const char *src, size_t srcCap void lobbsAppTimeAgo(uint32_t timestamp, char *buffer, size_t bufferSize); void lobbsAppTruncMsg(const char *message, char *buffer, size_t bufferSize, size_t maxLen); -bool lobbsAppLoadUser(LoBBSModule *mod, uint64_t uuid, LoScalar *outUser); +bool lobbsAppLoadUser(LoBBSKernel *kernel, uint64_t uuid, LoScalar *outUser); /** Username for display; uses "unknown" when missing. */ -void lobbsAppUsernameForUuid(LoBBSModule *mod, uint64_t uuid, char *buf, size_t bufCap); -uint64_t lobbsAppUuidForUsername(LoBBSModule *mod, const char *username); +void lobbsAppUsernameForUuid(LoBBSKernel *kernel, uint64_t uuid, char *buf, size_t bufCap); +uint64_t lobbsAppUuidForUsername(LoBBSKernel *kernel, const char *username); /** Sets uuidOut and returns true, or replies and returns false. */ bool lobbsAppResolveUsername(LoBBSCommandCtx &ctx, const char *username, uint64_t &uuidOut); diff --git a/apps/Auth/Auth.cpp b/apps/Auth/Auth.cpp index c29c766..5079565 100644 --- a/apps/Auth/Auth.cpp +++ b/apps/Auth/Auth.cpp @@ -1,6 +1,6 @@ #include "Auth.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" AuthApp::AuthApp(LoDb &lodb) : dal_(lodb) {} diff --git a/apps/Auth/AuthCommands.cpp b/apps/Auth/AuthCommands.cpp index 1d3f19c..abbdcc0 100644 --- a/apps/Auth/AuthCommands.cpp +++ b/apps/Auth/AuthCommands.cpp @@ -1,19 +1,18 @@ #include "AuthCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSResponse.h" #include "../AppUtil.h" #include "../Config/ConfigCommon.h" #include "AuthDal.h" #include "AuthRecords.h" -#include "mesh/NodeDB.h" #include #include #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static bool passwordLongEnough(LoBBSCommandCtx &ctx, const char *password) { @@ -56,7 +55,7 @@ static void handleWhoami(LoBBSCommandCtx &ctx) static void handleLogout(LoBBSCommandCtx &ctx) { - ctx.mod->auth().dal().logoutUser(ctx.session.nodeId); + ctx.kernel->auth().dal().logoutUser(ctx.session.nodeId); LoBBSResponse resp; lobbsRecordPush(resp.records, "Goodbye!"); lobbsCommandReplyResponse(ctx, resp); @@ -64,7 +63,7 @@ static void handleLogout(LoBBSCommandCtx &ctx) static void handleLogin(LoBBSCommandCtx &ctx) { - AuthDal &auth = ctx.mod->auth().dal(); + AuthDal &auth = ctx.kernel->auth().dal(); const char *username = lobbsArgShift(ctx); const char *password = lobbsArgShift(ctx); LoBBSResponse resp; @@ -90,13 +89,13 @@ static void handleLogin(LoBBSCommandCtx &ctx) lobbsCommandReplyResponse(ctx, resp); return; } - const uint32_t sessionKey = getFrom(ctx.mp); + const uint32_t sessionKey = ctx.inbound.from.key; if (!auth.loginUser(username, sessionKey)) { lobbsResponseSetError(resp, "Error creating session"); lobbsCommandReplyResponse(ctx, resp); return; } - } else if (LoDbError err = auth.createUser(username, password, getFrom(ctx.mp))) { + } else if (LoDbError err = auth.createUser(username, password, ctx.inbound.from.key)) { lobbsResponseSetError(resp, lobbsDbErrorText(err, "Error creating account")); lobbsCommandReplyResponse(ctx, resp); return; @@ -139,7 +138,7 @@ static bool passwdApply(AuthDal &auth, const char *username, const char *newPass static void handlePasswd(LoBBSCommandCtx &ctx) { - AuthDal &auth = ctx.mod->auth().dal(); + AuthDal &auth = ctx.kernel->auth().dal(); const char *a = lobbsArgShift(ctx); const char *b = lobbsArgShift(ctx); const char *c = lobbsArgShift(ctx); @@ -180,7 +179,7 @@ static void usersSubList(LoBBSCommandCtx &ctx) const char *sub = lobbsArgPeek(ctx); if (sub && strcasecmp(sub, "list") == 0) lobbsArgShift(ctx); - AuthDal &auth = ctx.mod->auth().dal(); + AuthDal &auth = ctx.kernel->auth().dal(); auto users = auth.listUsers(nullptr); LoBBSResponse resp; if (users.empty()) { @@ -206,7 +205,7 @@ static void usersSubFind(LoBBSCommandCtx &ctx) strncpy(filterBuf, lobbsArgRest(ctx), sizeof(filterBuf) - 1); filterBuf[sizeof(filterBuf) - 1] = '\0'; - AuthDal &auth = ctx.mod->auth().dal(); + AuthDal &auth = ctx.kernel->auth().dal(); auto users = auth.listUsers(filterBuf[0] ? filterBuf : nullptr); LoBBSResponse resp; if (users.empty()) { @@ -230,7 +229,7 @@ static void usersSubKick(LoBBSCommandCtx &ctx) return; } LoBBSResponse resp; - lobbsRecordPush(resp.records, ctx.mod->auth().dal().kickUserByUsername(user) ? "Sessions cleared." : "User not found."); + lobbsRecordPush(resp.records, ctx.kernel->auth().dal().kickUserByUsername(user) ? "Sessions cleared." : "User not found."); lobbsCommandReplyResponse(ctx, resp); } @@ -245,7 +244,7 @@ static void usersSubPromote(LoBBSCommandCtx &ctx) return; } LoBBSResponse resp; - LoDbError err = ctx.mod->auth().dal().setUserSysopByUsername(user, true); + LoDbError err = ctx.kernel->auth().dal().setUserSysopByUsername(user, true); lobbsRecordPush(resp.records, err == LODB_OK ? "Promoted." : lobbsDbErrorText(err, "User not found.")); lobbsCommandReplyResponse(ctx, resp); } @@ -260,7 +259,7 @@ static void usersSubDemote(LoBBSCommandCtx &ctx) lobbsCommandReplyResponse(ctx, resp); return; } - AuthDal &auth = ctx.mod->auth().dal(); + AuthDal &auth = ctx.kernel->auth().dal(); LoScalar target; LoBBSResponse resp; if (!auth.loadUserByUsername(user, &target)) { @@ -355,14 +354,14 @@ static void filterAuthConfigKeys(LoBBSCommandCtx *ctx, std::vector &ke static void actionAuthConfigChanged(LoBBSCommandCtx *ctx, const LoScalar &args) { - if (!ctx || !ctx->mod) + if (!ctx || !ctx->kernel) return; std::string key; if (!args.getString(LODB_F_TITLE, key)) return; if (key == "session.max" || key == "session.idle") { AuthDal::SessionConfig cfg = {lobbsConfigGet(*ctx, "session.max"), lobbsConfigGet(*ctx, "session.idle")}; - ctx->mod->auth().dal().applySessionConfig(cfg); + ctx->kernel->auth().dal().applySessionConfig(cfg); } } @@ -393,11 +392,11 @@ static void filterAuthHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const static void filterAuthStatusLines(LoBBSCommandCtx *ctx, std::vector &lines, const LoScalar &args) { (void)args; - if (!ctx || !ctx->mod) + if (!ctx || !ctx->kernel) return; char title[32]; char value[32]; - uint32_t n = ctx->mod->auth().dal().countAllUsers(); + uint32_t n = ctx->kernel->auth().dal().countAllUsers(); snprintf(title, sizeof(title), "Users"); snprintf(value, sizeof(value), "%u total", (unsigned)n); lobbsRecordPush(lines, title, value); @@ -435,8 +434,8 @@ static void displayAuthHuman(LoBBSCommandCtx *ctx, LoScalar &value, const LoScal static void actionAuthSeed(LoBBSCommandCtx *ctx, const LoScalar &args) { (void)args; - if (ctx && ctx->mod) - lobbsSeedAuth(*ctx->mod); + if (ctx && ctx->kernel) + lobbsSeedAuth(*ctx->kernel); } #endif diff --git a/apps/Auth/AuthDal.cpp b/apps/Auth/AuthDal.cpp index afa0aba..c20b568 100644 --- a/apps/Auth/AuthDal.cpp +++ b/apps/Auth/AuthDal.cpp @@ -1,16 +1,13 @@ #include "AuthDal.h" #include "AuthRecords.h" -#include "configuration.h" -#include "gps/RTC.h" -#include "mesh/NodeDB.h" -#include "mesh/Throttle.h" +#include "platforms/LoPlatform.h" #include #include #include #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void normalizeUsername(const char *username, char *normalized) { @@ -55,7 +52,7 @@ static lodb_uuid_t usernameToUuid(const char *username) { char normalized[LOBBS_USERNAME_BUFFER_SIZE]; normalizeUsername(username, normalized); - return lodb_new_uuid(normalized, nodeDB->getNodeNum()); + return lodb_new_uuid(normalized, lobbsPlatformLocalNodeId()); } static const char *stristrLocal(const char *haystack, const char *needle) @@ -141,17 +138,17 @@ bool AuthDal::loadUserByUsername(const char *username, LoScalar *user) lodb_uuid_t userUuid = usernameToUuid(username); LoDbError err = lodb_.get("users", userUuid, *user); if (err == LODB_OK) { - LOG_DEBUG("Loaded user by username: %s", username); + LOBBS_LOG_DEBUG("Loaded user by username: %s", username); return true; } - LOG_DEBUG("User not found: %s", username); + LOBBS_LOG_DEBUG("User not found: %s", username); return false; } void AuthDal::applySessionConfig(const SessionConfig &cfg) { if (cfg.maxSessions < sessions_.size()) { - const uint32_t now = millis(); + const uint32_t now = lobbsPlatformMillis(); std::sort(sessions_.begin(), sessions_.end(), [now](const Session &a, const Session &b) { if (a.used != b.used) return a.used; @@ -164,7 +161,8 @@ void AuthDal::applySessionConfig(const SessionConfig &cfg) bool AuthDal::sessionExpired(const Session &s) const { - return !Throttle::isWithinTimespanMs(s.lastActiveMs, sessionCfg_.idleSeconds * 1000u); + const uint32_t idleMs = sessionCfg_.idleSeconds * 1000u; + return (lobbsPlatformMillis() - s.lastActiveMs) >= idleMs; } AuthDal::Session *AuthDal::findSession(uint32_t nodeId) @@ -173,7 +171,7 @@ AuthDal::Session *AuthDal::findSession(uint32_t nodeId) if (!s.used || s.nodeId != nodeId) continue; if (sessionExpired(s)) { - LOG_INFO("Session for node 0x%08x expired", nodeId); + LOBBS_LOG_INFO("Session for node 0x%08x expired", nodeId); s = Session{}; return nullptr; } @@ -192,18 +190,18 @@ bool AuthDal::loadUserByNodeId(uint32_t nodeId, LoScalar *user, uint32_t *sessio { Session *s = findSession(nodeId); if (!s) { - LOG_DEBUG("No session found for node 0x%08x", nodeId); + LOBBS_LOG_DEBUG("No session found for node 0x%08x", nodeId); return false; } if (lodb_.get("users", s->userUuid, *user) != LODB_OK) { - LOG_WARN("Session 0x%08x references missing user, removing session", nodeId); + LOBBS_LOG_WARN("Session 0x%08x references missing user, removing session", nodeId); *s = Session{}; return false; } - s->lastActiveMs = millis(); - LOG_DEBUG("Loaded user by node ID: 0x%08x -> UUID: " LODB_UUID_FMT, nodeId, LODB_UUID_ARGS(s->userUuid)); + s->lastActiveMs = lobbsPlatformMillis(); + LOBBS_LOG_DEBUG("Loaded user by node ID: 0x%08x -> UUID: " LODB_UUID_FMT, nodeId, LODB_UUID_ARGS(s->userUuid)); if (sessionNodeIdOut) *sessionNodeIdOut = nodeId; if (authUserUuidOut) @@ -235,11 +233,11 @@ LoDbError AuthDal::createUser(const char *username, const char *password, uint32 user.setBool(AuthUser::FIELD_SYSOP, asSysop); LoDbError err = lodb_.insert("users", userUuid, user); if (err != LODB_OK) { - LOG_ERROR("Failed to create user: %s", username); + LOBBS_LOG_ERROR("Failed to create user: %s", username); return err; } - LOG_INFO("Created user: %s (sysop: %s)", username, asSysop ? "yes" : "no"); + LOBBS_LOG_INFO("Created user: %s (sysop: %s)", username, asSysop ? "yes" : "no"); return loginUser(username, nodeId) ? LODB_OK : LODB_ERR_INVALID; } @@ -258,7 +256,7 @@ bool AuthDal::loginUser(const char *username, uint32_t nodeId) { Session *s = findSession(nodeId); if (!s) { - const uint32_t now = millis(); + const uint32_t now = lobbsPlatformMillis(); uint32_t oldestAge = 0; for (auto &c : sessions_) { if (!c.used || sessionExpired(c)) { @@ -273,15 +271,15 @@ bool AuthDal::loginUser(const char *username, uint32_t nodeId) if (!s) return false; if (s->used && !sessionExpired(*s)) - LOG_INFO("Session table full, evicting node 0x%08x", s->nodeId); + LOBBS_LOG_INFO("Session table full, evicting node 0x%08x", s->nodeId); } *s = Session{}; s->used = true; s->nodeId = nodeId; s->userUuid = usernameToUuid(username); - s->lastActiveMs = millis(); - LOG_INFO("Created session for user %s on node 0x%08x", username, nodeId); + s->lastActiveMs = lobbsPlatformMillis(); + LOBBS_LOG_INFO("Created session for user %s on node 0x%08x", username, nodeId); return true; } @@ -290,10 +288,10 @@ bool AuthDal::logoutUser(uint32_t nodeId) Session *s = findSession(nodeId); if (s) { *s = Session{}; - LOG_INFO("Logged out node 0x%08x", nodeId); + LOBBS_LOG_INFO("Logged out node 0x%08x", nodeId); return true; } - LOG_WARN("No session found to log out for node 0x%08x", nodeId); + LOBBS_LOG_WARN("No session found to log out for node 0x%08x", nodeId); return false; } diff --git a/apps/Auth/AuthDal.h b/apps/Auth/AuthDal.h index f4afcb7..f8317ea 100644 --- a/apps/Auth/AuthDal.h +++ b/apps/Auth/AuthDal.h @@ -1,5 +1,5 @@ #pragma once -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #include #include #include diff --git a/apps/Auth/AuthSeed.cpp b/apps/Auth/AuthSeed.cpp index dda3e7a..8d876f0 100644 --- a/apps/Auth/AuthSeed.cpp +++ b/apps/Auth/AuthSeed.cpp @@ -1,16 +1,16 @@ -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #if LOBBS_SEED -#include "../../LoBBSModule.h" +#include "../../core/LoBBSKernel.h" #include "AuthDal.h" #include "AuthSeed.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -void lobbsSeedAuth(LoBBSModule &mod) +void lobbsSeedAuth(LoBBSKernel &kernel) { - AuthDal &auth = mod.auth().dal(); + AuthDal &auth = kernel.auth().dal(); static constexpr const char *kPass = "demo1"; auth.createUser("sysop", kPass, 0xDE000001, true); auth.logoutUser(0xDE000001); diff --git a/apps/Auth/AuthSeed.h b/apps/Auth/AuthSeed.h index 31fe5c7..aa4c836 100644 --- a/apps/Auth/AuthSeed.h +++ b/apps/Auth/AuthSeed.h @@ -1,7 +1,7 @@ #pragma once #if LOBBS_SEED -class LoBBSModule; -void lobbsSeedAuth(LoBBSModule &mod); +class LoBBSKernel; +void lobbsSeedAuth(LoBBSKernel &kernel); #endif diff --git a/apps/Config/Config.cpp b/apps/Config/Config.cpp index 0686dc0..97d3463 100644 --- a/apps/Config/Config.cpp +++ b/apps/Config/Config.cpp @@ -1,6 +1,6 @@ #include "Config.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" ConfigApp::ConfigApp(LoDb &lodb) : dal_(lodb) {} diff --git a/apps/Config/ConfigCommands.cpp b/apps/Config/ConfigCommands.cpp index 835fc05..8891295 100644 --- a/apps/Config/ConfigCommands.cpp +++ b/apps/Config/ConfigCommands.cpp @@ -1,9 +1,9 @@ #include "ConfigCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSReplyCache.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSReplyCache.h" +#include "../../core/LoBBSResponse.h" #include "../AppUtil.h" #include "ConfigCommon.h" #include "ConfigDal.h" @@ -12,7 +12,7 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static uint32_t gLobbsPagerTtlSec = LOBBS_REPLY_CACHE_TTL_SEC; @@ -63,7 +63,7 @@ static void handleConfig(LoBBSCommandCtx &ctx) { if (!lobbsCommandRequireSysop(ctx)) return; - ConfigDal &cfg = ctx.mod->config().dal(); + ConfigDal &cfg = ctx.kernel->config().dal(); const char *key = lobbsArgShift(ctx); if (!key) { LoBBSResponse resp; @@ -160,13 +160,13 @@ static void filterConfigHelpTopics(LoBBSCommandCtx *ctx, std::vector & static void filterConfigHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const LoScalar &args) { lobbsHelpForTable(ctx, value, args, "config", configHelpVerbs, sizeof(configHelpVerbs) / sizeof(configHelpVerbs[0])); - if (!ctx || !ctx->mod || value.has(LODB_F_DESCRIPTION) || !lobbsCtxIsSysop(*ctx)) + if (!ctx || !ctx->kernel || value.has(LODB_F_DESCRIPTION) || !lobbsCtxIsSysop(*ctx)) return; std::string query; if (!args.getString(LODB_F_TITLE, query) || strncasecmp(query.c_str(), "config ", 7) != 0) return; const char *key = query.c_str() + 7; - ConfigDal &cfg = ctx->mod->config().dal(); + ConfigDal &cfg = ctx->kernel->config().dal(); cfg.ensureRegistry(*ctx); const LoScalar *def = cfg.findKeyDef(key); if (!def) diff --git a/apps/Config/ConfigCommon.cpp b/apps/Config/ConfigCommon.cpp index 3f41d4b..6d86073 100644 --- a/apps/Config/ConfigCommon.cpp +++ b/apps/Config/ConfigCommon.cpp @@ -1,9 +1,9 @@ #include "ConfigCommon.h" -#include "../../LoBBSModule.h" +#include "../../core/LoBBSKernel.h" #include "ConfigDal.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" void lobbsConfigPushKey(std::vector &keys, const char *key, uint32_t def, uint32_t min, uint32_t max, const char *help) { @@ -21,8 +21,8 @@ void lobbsConfigPushKey(std::vector &keys, const char *key, uint32_t d uint32_t lobbsConfigGet(LoBBSCommandCtx &ctx, const char *key) { - if (!ctx.mod || !key) + if (!ctx.kernel || !key) return 0; - return ctx.mod->config().dal().effectiveValue(ctx, key); + return ctx.kernel->config().dal().effectiveValue(ctx, key); } diff --git a/apps/Config/ConfigCommon.h b/apps/Config/ConfigCommon.h index 6424f9e..9568a35 100644 --- a/apps/Config/ConfigCommon.h +++ b/apps/Config/ConfigCommon.h @@ -1,5 +1,5 @@ #pragma once -#include "../../LoBBSCommandCtx.h" +#include "../../core/LoBBSCommandCtx.h" #include "ConfigRecords.h" #include #include diff --git a/apps/Config/ConfigDal.cpp b/apps/Config/ConfigDal.cpp index 72bb71d..a7cb212 100644 --- a/apps/Config/ConfigDal.cpp +++ b/apps/Config/ConfigDal.cpp @@ -1,13 +1,13 @@ #include "ConfigDal.h" -#include "../../LoBBSCommandCtx.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" +#include "../../core/LoBBSCommandCtx.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" #include "../AppUtil.h" #include "ConfigRecords.h" #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" ConfigDal::ConfigDal(LoDb &lodb) : lodb_(lodb) { @@ -138,12 +138,12 @@ const char *ConfigDal::resetKey(LoBBSCommandCtx &ctx, const char *key) return nullptr; } -void ConfigDal::notifyDatabaseOpened(LoBBSModule &mod) +void ConfigDal::notifyDatabaseOpened(LoBBSKernel &kernel) { resetCache(); reloadOverrides(); LoBBSCommandCtx ctx; - ctx.mod = &mod; + ctx.kernel = &kernel; ensureRegistry(ctx); for (const LoScalar &rec : registry_) { std::string key; diff --git a/apps/Config/ConfigDal.h b/apps/Config/ConfigDal.h index 6376d6c..ff3de86 100644 --- a/apps/Config/ConfigDal.h +++ b/apps/Config/ConfigDal.h @@ -5,7 +5,7 @@ #include #include -class LoBBSModule; +class LoBBSKernel; struct LoBBSCommandCtx; class ConfigDal @@ -21,7 +21,7 @@ class ConfigDal uint32_t effectiveValue(LoBBSCommandCtx &ctx, const char *key); const char *validateAndSet(LoBBSCommandCtx &ctx, const char *key, uint32_t value); const char *resetKey(LoBBSCommandCtx &ctx, const char *key); - void notifyDatabaseOpened(LoBBSModule &mod); + void notifyDatabaseOpened(LoBBSKernel &kernel); private: lodb_uuid_t rowUuid(const char *key) const; diff --git a/apps/Fs/FsCommands.cpp b/apps/Fs/FsCommands.cpp index dfef596..a6f83c6 100644 --- a/apps/Fs/FsCommands.cpp +++ b/apps/Fs/FsCommands.cpp @@ -1,15 +1,13 @@ #include "FsCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSInstall.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSReply.h" -#include "../../LoBBSReplyCache.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSInstall.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSReply.h" +#include "../../core/LoBBSReplyCache.h" +#include "../../core/LoBBSResponse.h" #include "../AppUtil.h" -#include "mesh/NodeDB.h" -#include "mesh/Throttle.h" -#include +#include "platforms/LoPlatform.h" #include #include #include @@ -21,7 +19,7 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static constexpr int FS_LS_MAX_NAMES = 128; static constexpr size_t FS_NAME_BYTES = 48; @@ -162,7 +160,7 @@ static void fsSessionPrep(LoBBSCommandCtx &ctx) { if (LoFS::isDirectory(ctx.session.cwd)) return; - if (ctx.mod->auth().dal().setSessionCwd(ctx.session.nodeId, "/")) + if (ctx.kernel->auth().dal().setSessionCwd(ctx.session.nodeId, "/")) strncpy(ctx.session.cwd, "/", sizeof(ctx.session.cwd)); } @@ -227,7 +225,7 @@ static void handleLs(LoBBSCommandCtx &ctx) static void fsReplyFileText(LoBBSCommandCtx &ctx, const char *path, bool hex) { - File f = LoFS::open(path, FILE_O_READ); + LoFile f = LoFS::open(path, "r"); if (!f) { lobbsCommandReplyError(ctx, "No such file."); return; @@ -334,7 +332,7 @@ static void handleRm(LoBBSCommandCtx &ctx) return; } - File f = LoFS::open(spec, FILE_O_READ); + LoFile f = LoFS::open(spec, "r"); if (!f) { lobbsCommandReplyError(ctx, "No such file."); return; @@ -511,7 +509,7 @@ static void handleDf(LoBBSCommandCtx &ctx) struct Ctx { LoBBSResponse *resp; const char *installRoot; - } dc{&resp, lobbsInstallRoot(*ctx.mod)}; + } dc{&resp, lobbsInstallRoot(*ctx.kernel)}; LoFS::eachPresentMount( [](void *v, const char *name) { auto *dc = (Ctx *)v; @@ -570,18 +568,18 @@ static void handleFormat(LoBBSCommandCtx &ctx) lobbsCommandReplyError(ctx, "Refused: format SD cards on a PC."); return; } - const char *installRoot = lobbsInstallRoot(*ctx.mod); - const bool isFlash = strcmp(name, "flash") == 0; + const char *installRoot = lobbsInstallRoot(*ctx.kernel); + const bool isFlash = strcmp(name, "internal") == 0; const bool isInstall = installRoot[0] && strcmp(installRoot + 1, name) == 0; char msg[160]; if (!code) { static const char alphabet[] = "abcdefghjkmnpqrstuvwxyz23456789"; for (size_t i = 0; i < sizeof(fsFormatPending.code) - 1; i++) - fsFormatPending.code[i] = alphabet[random(sizeof(alphabet) - 1)]; + fsFormatPending.code[i] = alphabet[lobbsPlatformRandom(sizeof(alphabet) - 1)]; fsFormatPending.code[sizeof(fsFormatPending.code) - 1] = '\0'; fsFormatPending.nodeId = ctx.session.nodeId; - fsFormatPending.armedMs = millis(); + fsFormatPending.armedMs = lobbsPlatformMillis(); strncpy(fsFormatPending.mount, name, sizeof(fsFormatPending.mount) - 1); fsFormatPending.mount[sizeof(fsFormatPending.mount) - 1] = '\0'; char root[16], size[LO_HUMAN_BYTES_LEN]; @@ -595,7 +593,7 @@ static void handleFormat(LoBBSCommandCtx &ctx) const bool ok = fsFormatPending.code[0] && fsFormatPending.nodeId == ctx.session.nodeId && strcmp(fsFormatPending.mount, name) == 0 && strcmp(fsFormatPending.code, code) == 0 && - Throttle::isWithinTimespanMs(fsFormatPending.armedMs, FS_FORMAT_CONFIRM_MS); + (lobbsPlatformMillis() - fsFormatPending.armedMs) < FS_FORMAT_CONFIRM_MS; fsFormatPending.code[0] = '\0'; if (!ok) { snprintf(msg, sizeof(msg), "Wrong or expired code. Run /format %s for a new one.", name); @@ -606,12 +604,11 @@ static void handleFormat(LoBBSCommandCtx &ctx) lobbsCommandReplyError(ctx, "Format failed."); return; } - if (isFlash && nodeDB) - nodeDB->saveToDisk(); + lobbsPlatformAfterFormat(name); if (isInstall) - lobbsInstallInit(*ctx.mod); + lobbsInstallInit(*ctx.kernel); snprintf(msg, sizeof(msg), "Formatted /%s.%s", name, - lobbsInstallState(*ctx.mod) == LoBBSInstallState::Blank ? " LoBBS is blank. Run /install." : ""); + lobbsInstallState(*ctx.kernel) == LoBBSInstallState::Blank ? " LoBBS is blank. Run /install." : ""); lobbsCommandReply(ctx, msg); } @@ -659,12 +656,35 @@ static int fsB62DecodedByteCount(size_t charLen) } } -/** Decode one base62 chunk into out (max outCap). Returns byte count or -1. */ -static int fsB62DecodeChunk(const char *b62, uint8_t *out, size_t outCap) +static int fsB62WidthForBytes(int byteCount) +{ + switch (byteCount) { + case 1: + return 2; + case 2: + return 3; + case 3: + return 5; + case 4: + return 6; + case 5: + return 7; + case 6: + return 9; + case 7: + return 10; + case 8: + return 11; + default: + return -1; + } +} + +/** Decode one fixed-width base62 chunk into out. Returns byte count or -1. */ +static int fsB62DecodeChunkSpan(const char *b62, size_t charLen, uint8_t *out, size_t outCap) { if (!b62 || !out) return -1; - size_t charLen = strlen(b62); int byteCount = fsB62DecodedByteCount(charLen); if (byteCount < 0 || (size_t)byteCount > outCap) return -1; @@ -686,6 +706,34 @@ static int fsB62DecodeChunk(const char *b62, uint8_t *out, size_t outCap) return byteCount; } +/** Decode concatenated base62 segments (lobbs_chunkify.py format). */ +static int fsB62DecodePayload(const char *b62, uint8_t *out, size_t outCap) +{ + if (!b62 || !out) + return -1; + size_t p = 0; + size_t outPos = 0; + const size_t slen = strlen(b62); + while (p < slen) { + bool matched = false; + for (int k = 8; k >= 1; k--) { + const int w = fsB62WidthForBytes(k); + if (w < 0 || p + (size_t)w > slen) + continue; + const int n = fsB62DecodeChunkSpan(b62 + p, (size_t)w, out + outPos, outCap - outPos); + if (n == k) { + p += (size_t)w; + outPos += (size_t)n; + matched = true; + break; + } + } + if (!matched) + return -1; + } + return (int)outPos; +} + static bool fsParentIsDirectory(const char *path) { if (!path || path[0] != '/') @@ -917,12 +965,6 @@ static void handleUpload(LoBBSCommandCtx &ctx) return; } - int decodedLen = fsB62DecodeChunk(b62, fsUploadDecodeScratch, FS_UPLOAD_DECODE_MAX); - if (decodedLen < 0) { - lobbsCommandReplyError(ctx, "Bad data."); - return; - } - uint32_t curSize = 0; bool isDir = false; if (LoFS::stat(path, &curSize, &isDir)) { @@ -945,6 +987,12 @@ static void handleUpload(LoBBSCommandCtx &ctx) return; } + int decodedLen = fsB62DecodePayload(b62, fsUploadDecodeScratch, FS_UPLOAD_DECODE_MAX); + if (decodedLen < 0) { + lobbsCommandReplyError(ctx, "Bad data."); + return; + } + if (!LoFS::hasRoom(path, (uint32_t)decodedLen)) { lobbsCommandReplyError(ctx, "Disk full."); return; @@ -956,8 +1004,11 @@ static void handleUpload(LoBBSCommandCtx &ctx) uint32_t newSize = 0; if (!LoFS::stat(path, &newSize, &isDir) || isDir) { - lobbsCommandReplyError(ctx, "Failed."); - return; + newSize = offset + (uint32_t)decodedLen; + if (newSize < offset) { + lobbsCommandReplyError(ctx, "Failed."); + return; + } } char line[32]; snprintf(line, sizeof(line), "Size %u.", (unsigned)newSize); @@ -1009,7 +1060,7 @@ static void handleCd(LoBBSCommandCtx &ctx) lobbsCommandReplyError(ctx, "No such directory."); return; } - if (!ctx.mod->auth().dal().setSessionCwd(ctx.session.nodeId, path)) { + if (!ctx.kernel->auth().dal().setSessionCwd(ctx.session.nodeId, path)) { lobbsCommandReplyError(ctx, "Failed."); return; } diff --git a/apps/Help/HelpCommands.cpp b/apps/Help/HelpCommands.cpp index 491cc21..e112268 100644 --- a/apps/Help/HelpCommands.cpp +++ b/apps/Help/HelpCommands.cpp @@ -1,13 +1,13 @@ #include "HelpCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSConfig.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSResponse.h" -#include "../../LoBBSVersion.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSConfig.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSResponse.h" +#include "../../core/LoBBSVersion.h" #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void lobbsTrimRestInPlace(char *s) { diff --git a/apps/Mail/Mail.cpp b/apps/Mail/Mail.cpp index f63f08b..964ddc7 100644 --- a/apps/Mail/Mail.cpp +++ b/apps/Mail/Mail.cpp @@ -1,6 +1,6 @@ #include "Mail.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" MailApp::MailApp(LoDb &lodb) : dal_(lodb) {} diff --git a/apps/Mail/MailCommands.cpp b/apps/Mail/MailCommands.cpp index f4574dd..5b06f98 100644 --- a/apps/Mail/MailCommands.cpp +++ b/apps/Mail/MailCommands.cpp @@ -1,10 +1,10 @@ #include "MailCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSConfig.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSReply.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSConfig.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSReply.h" +#include "../../core/LoBBSResponse.h" #include "../AppUtil.h" #include "../Msg/MsgCommon.h" #include "MailDal.h" @@ -15,9 +15,9 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -static void mailAppendListRecord(LoBBSModule *mod, LoBBSResponse &resp, uint32_t oneBasedIndex, const LoScalar &mail) +static void mailAppendListRecord(LoBBSKernel *kernel, LoBBSResponse &resp, uint32_t oneBasedIndex, const LoScalar &mail) { char name[LOBBS_USERNAME_BUFFER_SIZE]; char when[LOBBS_TIME_AGO_BUFFER_SIZE]; @@ -25,7 +25,7 @@ static void mailAppendListRecord(LoBBSModule *mod, LoBBSResponse &resp, uint32_t // One char past the preview limit is enough for lobbsAppTruncMsg to add "...". char msg[LOBBS_LIST_LINE_TRUNC_MAX_CHARS + 2]; char line[LOBBS_REPLY_BYTES + 1]; - lobbsAppUsernameForUuid(mod, MailDal::mailFromUuid(mail), name, sizeof(name)); + lobbsAppUsernameForUuid(kernel, MailDal::mailFromUuid(mail), name, sizeof(name)); lobbsAppTimeAgo(MailDal::mailTimestamp(mail), when, sizeof(when)); MailDal::mailMessage(mail, msg, sizeof(msg)); lobbsAppTruncMsg(msg, trunc, sizeof(trunc), LOBBS_LIST_LINE_TRUNC_MAX_CHARS); @@ -57,7 +57,7 @@ static void mailSubList(LoBBSCommandCtx &ctx) return; } - auto all = ctx.mod->mail().dal().getAllMailForUser(inboxUuid); + auto all = ctx.kernel->mail().dal().getAllMailForUser(inboxUuid); LoBBSResponse resp; if (all.empty()) { lobbsResponseSetError(resp, "No mail"); @@ -65,13 +65,13 @@ static void mailSubList(LoBBSCommandCtx &ctx) return; } for (size_t i = 0; i < all.size(); i++) - mailAppendListRecord(ctx.mod, resp, (uint32_t)(i + 1), all[i]); + mailAppendListRecord(ctx.kernel, resp, (uint32_t)(i + 1), all[i]); lobbsCommandReplyResponse(ctx, resp); } static void mailSubRead(LoBBSCommandCtx &ctx, bool numericShorthand) { - MailDal &mail = ctx.mod->mail().dal(); + MailDal &mail = ctx.kernel->mail().dal(); if (!numericShorthand) { const char *sub = lobbsArgShift(ctx); (void)sub; @@ -116,7 +116,7 @@ static void mailSubRead(LoBBSCommandCtx &ctx, bool numericShorthand) char when[LOBBS_TIME_AGO_BUFFER_SIZE]; char body[LOBBS_MESSAGE_READ_BODY_BUFFER_SIZE]; char header[LOBBS_USERNAME_BUFFER_SIZE + LOBBS_TIME_AGO_BUFFER_SIZE + 16]; - lobbsAppUsernameForUuid(ctx.mod, MailDal::mailFromUuid(m), name, sizeof(name)); + lobbsAppUsernameForUuid(ctx.kernel, MailDal::mailFromUuid(m), name, sizeof(name)); MailDal::mailMessage(m, body, sizeof(body)); lobbsAppTimeAgo(MailDal::mailTimestamp(m), when, sizeof(when)); snprintf(header, sizeof(header), "From: @%s (%s)", name, when); @@ -144,7 +144,7 @@ static void mailSubUnread(LoBBSCommandCtx &ctx) uint32_t idx = 0; if (!lobbsMsgShiftIndex(ctx, 1, "Usage: /mail unread N", "Invalid message number.", idx)) return; - MailDal &mail = ctx.mod->mail().dal(); + MailDal &mail = ctx.kernel->mail().dal(); auto mailMessages = mail.getAllMailForUser(lobbsCtxUserUuid(ctx)); if (idx == 0 || idx > mailMessages.size()) { LoBBSResponse resp; @@ -165,7 +165,7 @@ static void mailSubDelete(LoBBSCommandCtx &ctx) return; LoBBSResponse resp; lobbsRecordPush(resp.records, - ctx.mod->mail().dal().deleteMailInboxIndex(lobbsCtxUserUuid(ctx), idx) ? "Deleted." : "Failed."); + ctx.kernel->mail().dal().deleteMailInboxIndex(lobbsCtxUserUuid(ctx), idx) ? "Deleted." : "Failed."); lobbsCommandReplyResponse(ctx, resp); } @@ -190,7 +190,7 @@ static void mailSubSend(LoBBSCommandCtx &ctx) if (!lobbsAppResolveUsername(ctx, user, toUuid)) return; LoBBSResponse resp; - LoDbError err = ctx.mod->mail().dal().sendMail(lobbsCtxUserUuid(ctx), toUuid, body); + LoDbError err = ctx.kernel->mail().dal().sendMail(lobbsCtxUserUuid(ctx), toUuid, body); lobbsRecordPush(resp.records, err == LODB_OK ? "Mail sent." : lobbsDbErrorText(err, "Failed to send mail.")); lobbsCommandReplyResponse(ctx, resp); } @@ -245,16 +245,16 @@ static void filterMailHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const static void filterMailStatusLines(LoBBSCommandCtx *ctx, std::vector &lines, const LoScalar &args) { (void)args; - if (!ctx || !ctx->mod) + if (!ctx || !ctx->kernel) return; char title[64]; char value[32]; if (lobbsCtxLoggedIn(*ctx)) { - uint32_t n = ctx->mod->mail().dal().countUnreadMail(lobbsCtxUserUuid(*ctx)); + uint32_t n = ctx->kernel->mail().dal().countUnreadMail(lobbsCtxUserUuid(*ctx)); snprintf(title, sizeof(title), "Mail"); snprintf(value, sizeof(value), "%u unread", (unsigned)n); } else { - uint32_t n = ctx->mod->mail().dal().countAllMail(); + uint32_t n = ctx->kernel->mail().dal().countAllMail(); snprintf(title, sizeof(title), "Mail"); snprintf(value, sizeof(value), "%u total", (unsigned)n); } @@ -266,8 +266,8 @@ static void filterMailStatusLines(LoBBSCommandCtx *ctx, std::vector &l static void actionMailSeed(LoBBSCommandCtx *ctx, const LoScalar &args) { (void)args; - if (ctx && ctx->mod) - lobbsSeedMail(*ctx->mod); + if (ctx && ctx->kernel) + lobbsSeedMail(*ctx->kernel); } #endif diff --git a/apps/Mail/MailDal.cpp b/apps/Mail/MailDal.cpp index e57a48d..b427306 100644 --- a/apps/Mail/MailDal.cpp +++ b/apps/Mail/MailDal.cpp @@ -1,11 +1,10 @@ #include "MailDal.h" -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #include "MailRecords.h" -#include "configuration.h" -#include "gps/RTC.h" +#include "platforms/LoPlatform.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" MailDal::MailDal(LoDb &lodb) : lodb_(lodb) { @@ -85,7 +84,7 @@ LoDbError MailDal::sendMail(uint64_t fromUserUuid, uint64_t toUserUuid, const ch LoDbError err = lodb_.insert("mail", mailUuidVal, mail); if (err != LODB_OK) - LOG_ERROR("Failed to send mail"); + LOBBS_LOG_ERROR("Failed to send mail"); return err; } diff --git a/apps/Mail/MailSeed.cpp b/apps/Mail/MailSeed.cpp index e9c483c..c15d923 100644 --- a/apps/Mail/MailSeed.cpp +++ b/apps/Mail/MailSeed.cpp @@ -1,26 +1,26 @@ -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #if LOBBS_SEED -#include "../../LoBBSModule.h" +#include "../../core/LoBBSKernel.h" #include "../AppUtil.h" #include "MailDal.h" #include "MailSeed.h" #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -void lobbsSeedMail(LoBBSModule &mod) +void lobbsSeedMail(LoBBSKernel &kernel) { - MailDal &mail = mod.mail().dal(); - uint64_t sysop = lobbsAppUuidForUsername(&mod, "sysop"); + MailDal &mail = kernel.mail().dal(); + uint64_t sysop = lobbsAppUuidForUsername(&kernel, "sysop"); if (!sysop) return; for (int i = 1; i <= 8; i++) { char fromName[16]; snprintf(fromName, sizeof(fromName), "demo%02d", i); - uint64_t fromUuid = lobbsAppUuidForUsername(&mod, fromName); + uint64_t fromUuid = lobbsAppUuidForUsername(&kernel, fromName); if (!fromUuid) continue; char body[64]; diff --git a/apps/Mail/MailSeed.h b/apps/Mail/MailSeed.h index 39ed5e5..fabcac0 100644 --- a/apps/Mail/MailSeed.h +++ b/apps/Mail/MailSeed.h @@ -1,7 +1,7 @@ #pragma once #if LOBBS_SEED -class LoBBSModule; -void lobbsSeedMail(LoBBSModule &mod); +class LoBBSKernel; +void lobbsSeedMail(LoBBSKernel &kernel); #endif diff --git a/apps/Msg/MsgCommon.cpp b/apps/Msg/MsgCommon.cpp index 1dc5d44..d618ca9 100644 --- a/apps/Msg/MsgCommon.cpp +++ b/apps/Msg/MsgCommon.cpp @@ -1,13 +1,13 @@ #include "MsgCommon.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSResponse.h" #include "../Mail/MailRecords.h" #include "../News/NewsRecords.h" #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" bool lobbsMsgShiftIndex(LoBBSCommandCtx &ctx, size_t count, const char *usage, const char *badNumMsg, uint32_t &idxOut) { diff --git a/apps/Msg/MsgCommon.h b/apps/Msg/MsgCommon.h index b30e130..d0c6102 100644 --- a/apps/Msg/MsgCommon.h +++ b/apps/Msg/MsgCommon.h @@ -1,5 +1,5 @@ #pragma once -#include "../../LoBBSCommandCtx.h" +#include "../../core/LoBBSCommandCtx.h" #include #include diff --git a/apps/News/News.cpp b/apps/News/News.cpp index 138fa59..3568a8d 100644 --- a/apps/News/News.cpp +++ b/apps/News/News.cpp @@ -1,6 +1,6 @@ #include "News.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" NewsApp::NewsApp(LoDb &lodb) : dal_(lodb) {} diff --git a/apps/News/NewsCommands.cpp b/apps/News/NewsCommands.cpp index 6a7fd61..4af9b4c 100644 --- a/apps/News/NewsCommands.cpp +++ b/apps/News/NewsCommands.cpp @@ -1,10 +1,10 @@ #include "NewsCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSConfig.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSReply.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSConfig.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSReply.h" +#include "../../core/LoBBSResponse.h" #include "../AppUtil.h" #include "../Msg/MsgCommon.h" #include "NewsDal.h" @@ -16,9 +16,9 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -static void newsAppendListRecord(LoBBSModule *mod, LoBBSResponse &resp, uint32_t oneBasedIndex, const LoBBSNewsEntry &entry) +static void newsAppendListRecord(LoBBSKernel *kernel, LoBBSResponse &resp, uint32_t oneBasedIndex, const LoBBSNewsEntry &entry) { const LoScalar &news = entry.news; char name[LOBBS_USERNAME_BUFFER_SIZE]; @@ -27,7 +27,7 @@ static void newsAppendListRecord(LoBBSModule *mod, LoBBSResponse &resp, uint32_t // One char past the preview limit is enough for lobbsAppTruncMsg to add "...". char msg[LOBBS_LIST_LINE_TRUNC_MAX_CHARS + 2]; char line[LOBBS_REPLY_BYTES + 1]; - lobbsAppUsernameForUuid(mod, NewsDal::newsAuthorUuid(news), name, sizeof(name)); + lobbsAppUsernameForUuid(kernel, NewsDal::newsAuthorUuid(news), name, sizeof(name)); lobbsAppTimeAgo(NewsDal::newsTimestamp(news), when, sizeof(when)); NewsDal::newsMessage(news, msg, sizeof(msg)); lobbsAppTruncMsg(msg, trunc, sizeof(trunc), LOBBS_LIST_LINE_TRUNC_MAX_CHARS); @@ -48,7 +48,7 @@ static void newsSubList(LoBBSCommandCtx &ctx) if (sub && strcasecmp(sub, "list") == 0) lobbsArgShift(ctx); - auto all = ctx.mod->news().dal().getAllNewsForUser(lobbsCtxUserUuid(ctx)); + auto all = ctx.kernel->news().dal().getAllNewsForUser(lobbsCtxUserUuid(ctx)); LoBBSResponse resp; if (all.empty()) { lobbsResponseSetError(resp, "No news"); @@ -56,7 +56,7 @@ static void newsSubList(LoBBSCommandCtx &ctx) return; } for (size_t i = 0; i < all.size(); i++) - newsAppendListRecord(ctx.mod, resp, (uint32_t)(i + 1), all[i]); + newsAppendListRecord(ctx.kernel, resp, (uint32_t)(i + 1), all[i]); lobbsCommandReplyResponse(ctx, resp); } @@ -72,7 +72,7 @@ static void newsSubRead(LoBBSCommandCtx &ctx, bool numericShorthand) lobbsCommandReplyResponse(ctx, resp); return; } - NewsDal &news = ctx.mod->news().dal(); + NewsDal &news = ctx.kernel->news().dal(); auto newsItems = news.getAllNewsForUser(lobbsCtxUserUuid(ctx)); if (idx == 0 || idx > newsItems.size()) { LoBBSResponse resp; @@ -85,7 +85,7 @@ static void newsSubRead(LoBBSCommandCtx &ctx, bool numericShorthand) char when[LOBBS_TIME_AGO_BUFFER_SIZE]; char body[LOBBS_MESSAGE_READ_BODY_BUFFER_SIZE]; char header[LOBBS_USERNAME_BUFFER_SIZE + LOBBS_TIME_AGO_BUFFER_SIZE + 16]; - lobbsAppUsernameForUuid(ctx.mod, NewsDal::newsAuthorUuid(item), name, sizeof(name)); + lobbsAppUsernameForUuid(ctx.kernel, NewsDal::newsAuthorUuid(item), name, sizeof(name)); NewsDal::newsMessage(item, body, sizeof(body)); lobbsAppTimeAgo(NewsDal::newsTimestamp(item), when, sizeof(when)); snprintf(header, sizeof(header), "From: @%s (%s)", name, when); @@ -107,7 +107,7 @@ static void newsSubUnread(LoBBSCommandCtx &ctx) uint32_t idx = 0; if (!lobbsMsgShiftIndex(ctx, 1, "Usage: /news unread N", "Invalid news number.", idx)) return; - NewsDal &news = ctx.mod->news().dal(); + NewsDal &news = ctx.kernel->news().dal(); auto newsItems = news.getAllNewsForUser(lobbsCtxUserUuid(ctx)); if (idx == 0 || idx > newsItems.size()) { LoBBSResponse resp; @@ -126,7 +126,7 @@ static void newsSubDelete(LoBBSCommandCtx &ctx) uint32_t idx = 0; if (!lobbsMsgShiftIndex(ctx, 1, "Usage: /news delete N", "Invalid news number.", idx)) return; - NewsDal &news = ctx.mod->news().dal(); + NewsDal &news = ctx.kernel->news().dal(); auto newsItems = news.getAllNewsForUser(lobbsCtxUserUuid(ctx)); if (idx == 0 || idx > newsItems.size()) { LoBBSResponse resp; @@ -163,7 +163,7 @@ static void newsSubPost(LoBBSCommandCtx &ctx) return; } LoBBSResponse resp; - LoDbError err = ctx.mod->news().dal().postNews(lobbsCtxUserUuid(ctx), msgBody); + LoDbError err = ctx.kernel->news().dal().postNews(lobbsCtxUserUuid(ctx), msgBody); lobbsRecordPush(resp.records, err == LODB_OK ? "News posted." : lobbsDbErrorText(err, "Failed to post news.")); lobbsCommandReplyResponse(ctx, resp); } @@ -221,16 +221,16 @@ static void filterNewsHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const static void filterNewsStatusLines(LoBBSCommandCtx *ctx, std::vector &lines, const LoScalar &args) { (void)args; - if (!ctx || !ctx->mod) + if (!ctx || !ctx->kernel) return; char title[64]; char value[32]; if (lobbsCtxLoggedIn(*ctx)) { - uint32_t n = ctx->mod->news().dal().countUnreadNews(lobbsCtxUserUuid(*ctx)); + uint32_t n = ctx->kernel->news().dal().countUnreadNews(lobbsCtxUserUuid(*ctx)); snprintf(title, sizeof(title), "News"); snprintf(value, sizeof(value), "%u unread", (unsigned)n); } else { - uint32_t n = ctx->mod->news().dal().countAllNews(); + uint32_t n = ctx->kernel->news().dal().countAllNews(); snprintf(title, sizeof(title), "News"); snprintf(value, sizeof(value), "%u total", (unsigned)n); } @@ -242,8 +242,8 @@ static void filterNewsStatusLines(LoBBSCommandCtx *ctx, std::vector &l static void actionNewsSeed(LoBBSCommandCtx *ctx, const LoScalar &args) { (void)args; - if (ctx && ctx->mod) - lobbsSeedNews(*ctx->mod); + if (ctx && ctx->kernel) + lobbsSeedNews(*ctx->kernel); } #endif diff --git a/apps/News/NewsDal.cpp b/apps/News/NewsDal.cpp index 186e35a..9ef71d0 100644 --- a/apps/News/NewsDal.cpp +++ b/apps/News/NewsDal.cpp @@ -1,12 +1,11 @@ #include "NewsDal.h" -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #include "NewsRecords.h" -#include "configuration.h" -#include "gps/RTC.h" +#include "platforms/LoPlatform.h" #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" NewsDal::NewsDal(LoDb &lodb) : lodb_(lodb) { @@ -58,7 +57,7 @@ static constexpr uint32_t LOBBS_MAX_LIST_ROWS = 256; LoDbError NewsDal::postNews(uint64_t authorUserUuid, const char *message) { - lodb_uuid_t newsUuidVal = lodb_new_uuid(nullptr, authorUserUuid ^ (uint64_t)getTime()); + lodb_uuid_t newsUuidVal = lodb_new_uuid(nullptr, authorUserUuid ^ (uint64_t)lobbsPlatformUnixNow()); LoScalar news; char msgBuf[LOBBS_MESSAGE_BODY_BUFFER_SIZE]; diff --git a/apps/News/NewsSeed.cpp b/apps/News/NewsSeed.cpp index 43005c3..c4b6fd7 100644 --- a/apps/News/NewsSeed.cpp +++ b/apps/News/NewsSeed.cpp @@ -1,21 +1,21 @@ -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #if LOBBS_SEED -#include "../../LoBBSModule.h" +#include "../../core/LoBBSKernel.h" #include "../AppUtil.h" #include "NewsDal.h" #include "NewsSeed.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -void lobbsSeedNews(LoBBSModule &mod) +void lobbsSeedNews(LoBBSKernel &kernel) { - NewsDal &news = mod.news().dal(); + NewsDal &news = kernel.news().dal(); for (int i = 1; i <= 10; i++) { char authorName[16]; snprintf(authorName, sizeof(authorName), "demo%02d", ((i - 1) % 12) + 1); - uint64_t author = lobbsAppUuidForUsername(&mod, authorName); + uint64_t author = lobbsAppUuidForUsername(&kernel, authorName); if (!author) continue; char body[80]; diff --git a/apps/News/NewsSeed.h b/apps/News/NewsSeed.h index a1cd708..2e34897 100644 --- a/apps/News/NewsSeed.h +++ b/apps/News/NewsSeed.h @@ -1,7 +1,7 @@ #pragma once #if LOBBS_SEED -class LoBBSModule; -void lobbsSeedNews(LoBBSModule &mod); +class LoBBSKernel; +void lobbsSeedNews(LoBBSKernel &kernel); #endif diff --git a/apps/Status/StatusCommands.cpp b/apps/Status/StatusCommands.cpp index 53079a5..8c690f7 100644 --- a/apps/Status/StatusCommands.cpp +++ b/apps/Status/StatusCommands.cpp @@ -1,9 +1,9 @@ #include "StatusCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSResponse.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void handleStatus(LoBBSCommandCtx &ctx) { diff --git a/apps/Time/TimeCommands.cpp b/apps/Time/TimeCommands.cpp index 8f2dfb5..1de3e4a 100644 --- a/apps/Time/TimeCommands.cpp +++ b/apps/Time/TimeCommands.cpp @@ -1,23 +1,22 @@ #include "TimeCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSReply.h" -#include "../../LoBBSResponse.h" -#include "gps/RTC.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSReply.h" +#include "../../core/LoBBSResponse.h" +#include "platforms/LoPlatform.h" #include #include #include #include -#include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void handleTime(LoBBSCommandCtx &ctx) { const char *peek = lobbsArgPeek(ctx); if (!peek) { char buf[LOBBS_REPLY_BYTES + 1]; - snprintf(buf, sizeof(buf), "Time: %u (%s)", (unsigned)getTime(), RtcName(getRTCQuality())); + snprintf(buf, sizeof(buf), "Time: %u (%s)", (unsigned)lobbsPlatformUnixNow(), lobbsPlatformClockName()); lobbsCommandReply(ctx, buf); return; } @@ -32,13 +31,10 @@ static void handleTime(LoBBSCommandCtx &ctx) lobbsCommandReplyError(ctx, "Usage: /time\nUsage: /time unix (sysop)"); return; } - struct timeval tv; - tv.tv_sec = (time_t)sec; - tv.tv_usec = 0; - RTCSetResult r = perhapsSetRTC(RTCQualityNTP, &tv, true); - if (r == RTCSetResultSuccess) + LoPlatformSetUnixResult r = lobbsPlatformSetUnix(sec); + if (r == LoPlatformSetUnixResult::Ok) lobbsCommandReply(ctx, "Time set."); - else if (r == RTCSetResultInvalidTime) + else if (r == LoPlatformSetUnixResult::Invalid) lobbsCommandReplyError(ctx, "Invalid time."); else lobbsCommandReplyError(ctx, "Could not set time."); @@ -74,4 +70,3 @@ void lobbsTimeRegisterCommands() lobbsAddFilter("help_topics", filterTimeHelpTopics, LOBBS_HOOK_PRIORITY_TIME); lobbsAddFilter("help_for_topic", filterTimeHelpForTopic, LOBBS_HOOK_PRIORITY_TIME); } - diff --git a/apps/Wall/Wall.cpp b/apps/Wall/Wall.cpp index 51955ff..e6c4014 100644 --- a/apps/Wall/Wall.cpp +++ b/apps/Wall/Wall.cpp @@ -1,6 +1,6 @@ #include "Wall.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" WallApp::WallApp(LoDb &lodb) : dal_(lodb) {} diff --git a/apps/Wall/WallCommands.cpp b/apps/Wall/WallCommands.cpp index 6152191..3495b11 100644 --- a/apps/Wall/WallCommands.cpp +++ b/apps/Wall/WallCommands.cpp @@ -1,9 +1,9 @@ #include "WallCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSReply.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSReply.h" +#include "../../core/LoBBSResponse.h" #include "../Config/ConfigCommon.h" #include "WallDal.h" #include @@ -11,11 +11,11 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void replyGrid(LoBBSCommandCtx &ctx, bool markSeen) { - WallDal &wall = ctx.mod->wall().dal(); + WallDal &wall = ctx.kernel->wall().dal(); char buf[LOBBS_REPLY_BYTES + 1]; if (!wall.formatGridLines(buf, sizeof(buf))) { lobbsCommandReplyError(ctx, "Canvas error."); @@ -55,7 +55,7 @@ static void handleWall(LoBBSCommandCtx &ctx) const uint32_t period = lobbsConfigGet(ctx, "wall.period"); const uint32_t cells = lobbsConfigGet(ctx, "wall.cells"); if (const char *err = - ctx.mod->wall().dal().applyPaintTokens(lobbsCtxUserUuid(ctx), ctx.session.isSysop, toks, n, period, cells)) { + ctx.kernel->wall().dal().applyPaintTokens(lobbsCtxUserUuid(ctx), ctx.session.isSysop, toks, n, period, cells)) { lobbsCommandReplyError(ctx, err); return; } @@ -84,9 +84,9 @@ static void filterWallHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const static void filterWallStatusLines(LoBBSCommandCtx *ctx, std::vector &lines, const LoScalar &args) { (void)args; - if (!ctx || !ctx->mod || !lobbsCtxLoggedIn(*ctx)) + if (!ctx || !ctx->kernel || !lobbsCtxLoggedIn(*ctx)) return; - bool dirty = ctx->mod->wall().dal().isDirtyForUser(lobbsCtxUserUuid(*ctx)); + bool dirty = ctx->kernel->wall().dal().isDirtyForUser(lobbsCtxUserUuid(*ctx)); lobbsRecordPush(lines, "Wall", dirty ? "new" : "seen"); } @@ -95,8 +95,8 @@ static void filterWallStatusLines(LoBBSCommandCtx *ctx, std::vector &l static void actionWallSeed(LoBBSCommandCtx *ctx, const LoScalar &args) { (void)args; - if (ctx && ctx->mod) - lobbsSeedWall(*ctx->mod); + if (ctx && ctx->kernel) + lobbsSeedWall(*ctx->kernel); } #endif diff --git a/apps/Wall/WallDal.cpp b/apps/Wall/WallDal.cpp index 7860f09..982c3f2 100644 --- a/apps/Wall/WallDal.cpp +++ b/apps/Wall/WallDal.cpp @@ -5,7 +5,7 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" WallDal::WallDal(LoDb &lodb) : lodb_(lodb) { diff --git a/apps/Wall/WallSeed.cpp b/apps/Wall/WallSeed.cpp index a88c849..af4c08c 100644 --- a/apps/Wall/WallSeed.cpp +++ b/apps/Wall/WallSeed.cpp @@ -1,17 +1,17 @@ -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #if LOBBS_SEED -#include "../../LoBBSModule.h" +#include "../../core/LoBBSKernel.h" #include "../AppUtil.h" #include "WallDal.h" #include "WallSeed.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -void lobbsSeedWall(LoBBSModule &mod) +void lobbsSeedWall(LoBBSKernel &kernel) { - WallDal &wall = mod.wall().dal(); - uint64_t sysop = lobbsAppUuidForUsername(&mod, "sysop"); + WallDal &wall = kernel.wall().dal(); + uint64_t sysop = lobbsAppUuidForUsername(&kernel, "sysop"); if (!sysop) return; const char *tokens[] = {"a1#", "a2#", "b2#", "c3x", "d4y"}; diff --git a/apps/Wall/WallSeed.h b/apps/Wall/WallSeed.h index d310a88..716a023 100644 --- a/apps/Wall/WallSeed.h +++ b/apps/Wall/WallSeed.h @@ -1,7 +1,7 @@ #pragma once #if LOBBS_SEED -class LoBBSModule; -void lobbsSeedWall(LoBBSModule &mod); +class LoBBSKernel; +void lobbsSeedWall(LoBBSKernel &kernel); #endif diff --git a/apps/Yarn/Yarn.cpp b/apps/Yarn/Yarn.cpp index 57fd477..aa043c9 100644 --- a/apps/Yarn/Yarn.cpp +++ b/apps/Yarn/Yarn.cpp @@ -1,7 +1,7 @@ #include "Yarn.h" -#include "../../LoBBSBootTrace.h" -#include "LoBBSStackGuard.h" +#include "../../core/LoBBSBootTrace.h" +#include "core/LoBBSStackGuard.h" YarnApp::YarnApp(LoDb &lodb) : dal_(lodb) { diff --git a/apps/Yarn/YarnCommands.cpp b/apps/Yarn/YarnCommands.cpp index 8546246..d2d6307 100644 --- a/apps/Yarn/YarnCommands.cpp +++ b/apps/Yarn/YarnCommands.cpp @@ -1,9 +1,9 @@ #include "YarnCommands.h" -#include "../../LoBBSCommandRegistry.h" -#include "../../LoBBSHooks.h" -#include "../../LoBBSModule.h" -#include "../../LoBBSReply.h" -#include "../../LoBBSResponse.h" +#include "../../core/LoBBSCommandRegistry.h" +#include "../../core/LoBBSHooks.h" +#include "../../core/LoBBSKernel.h" +#include "../../core/LoBBSReply.h" +#include "../../core/LoBBSResponse.h" #include "../Config/ConfigCommon.h" #include "YarnDal.h" #include @@ -11,17 +11,17 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void replyYarnView(LoBBSCommandCtx &ctx, bool markSeen) { char buf[LOBBS_REPLY_BYTES + 1]; - if (!ctx.mod->yarn().dal().formatYarnView(buf, sizeof(buf))) { + if (!ctx.kernel->yarn().dal().formatYarnView(buf, sizeof(buf))) { lobbsCommandReplyError(ctx, "Yarn error."); return; } if (markSeen && lobbsCtxLoggedIn(ctx)) - ctx.mod->yarn().dal().markYarnSeen(lobbsCtxUserUuid(ctx)); + ctx.kernel->yarn().dal().markYarnSeen(lobbsCtxUserUuid(ctx)); lobbsCommandReply(ctx, buf); } @@ -55,7 +55,7 @@ static void handleYarn(LoBBSCommandCtx &ctx) const uint32_t words = lobbsConfigGet(ctx, "yarn.words"); const uint32_t chars = lobbsConfigGet(ctx, "yarn.chars"); if (const char *err = - ctx.mod->yarn().dal().appendWords(lobbsCtxUserUuid(ctx), ctx.session.isSysop, toks, n, period, words, chars)) { + ctx.kernel->yarn().dal().appendWords(lobbsCtxUserUuid(ctx), ctx.session.isSysop, toks, n, period, words, chars)) { lobbsCommandReplyError(ctx, err); return; } @@ -84,14 +84,14 @@ static void filterYarnHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const static void filterYarnStatusLines(LoBBSCommandCtx *ctx, std::vector &lines, const LoScalar &args) { (void)args; - if (!ctx || !ctx->mod) + if (!ctx || !ctx->kernel) return; char value[32]; if (lobbsCtxLoggedIn(*ctx)) { - uint32_t n = ctx->mod->yarn().dal().newWordsForUser(lobbsCtxUserUuid(*ctx)); + uint32_t n = ctx->kernel->yarn().dal().newWordsForUser(lobbsCtxUserUuid(*ctx)); snprintf(value, sizeof(value), "%u new words", (unsigned)n); } else { - uint32_t n = ctx->mod->yarn().dal().totalWordsAppended(); + uint32_t n = ctx->kernel->yarn().dal().totalWordsAppended(); snprintf(value, sizeof(value), "%u words total", (unsigned)n); } lobbsRecordPush(lines, "Yarn", value); @@ -102,8 +102,8 @@ static void filterYarnStatusLines(LoBBSCommandCtx *ctx, std::vector &l static void actionYarnSeed(LoBBSCommandCtx *ctx, const LoScalar &args) { (void)args; - if (ctx && ctx->mod) - lobbsSeedYarn(*ctx->mod); + if (ctx && ctx->kernel) + lobbsSeedYarn(*ctx->kernel); } #endif diff --git a/apps/Yarn/YarnDal.cpp b/apps/Yarn/YarnDal.cpp index fe1c88e..1288e43 100644 --- a/apps/Yarn/YarnDal.cpp +++ b/apps/Yarn/YarnDal.cpp @@ -5,7 +5,7 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" YarnDal::YarnDal(LoDb &lodb) : lodb_(lodb) { diff --git a/apps/Yarn/YarnSeed.cpp b/apps/Yarn/YarnSeed.cpp index bbecd32..561db06 100644 --- a/apps/Yarn/YarnSeed.cpp +++ b/apps/Yarn/YarnSeed.cpp @@ -1,17 +1,17 @@ -#include "../../LoBBSConfig.h" +#include "../../core/LoBBSConfig.h" #if LOBBS_SEED -#include "../../LoBBSModule.h" +#include "../../core/LoBBSKernel.h" #include "../AppUtil.h" #include "YarnDal.h" #include "YarnSeed.h" -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" -void lobbsSeedYarn(LoBBSModule &mod) +void lobbsSeedYarn(LoBBSKernel &kernel) { - YarnDal &yarn = mod.yarn().dal(); - uint64_t sysop = lobbsAppUuidForUsername(&mod, "sysop"); + YarnDal &yarn = kernel.yarn().dal(); + uint64_t sysop = lobbsAppUuidForUsername(&kernel, "sysop"); if (!sysop) return; const char *words1[] = {"Demo", "yarn", "seed", "line", "one."}; diff --git a/apps/Yarn/YarnSeed.h b/apps/Yarn/YarnSeed.h index 53c3a9b..42b8965 100644 --- a/apps/Yarn/YarnSeed.h +++ b/apps/Yarn/YarnSeed.h @@ -1,7 +1,7 @@ #pragma once #if LOBBS_SEED -class LoBBSModule; -void lobbsSeedYarn(LoBBSModule &mod); +class LoBBSKernel; +void lobbsSeedYarn(LoBBSKernel &kernel); #endif diff --git a/ci/README.md b/ci/README.md new file mode 100644 index 0000000..76d1e75 --- /dev/null +++ b/ci/README.md @@ -0,0 +1,25 @@ +# LoBBS CI helpers + +Host-side checks in the plugin repo without flashing firmware. + +## Host protocol tests + +```bash +cd ci/host +pio test -e host -f test_protocol_host +``` + +Uses `LOBBS_PLATFORM_NATIVE` (not Meshtastic or MeshCore). Build output lives under `ci/host/.pio/` (gitignored). + +## Command integration tests + +Run from the **Meshtastic fork** (full kernel + LoFS + apps against upstream’s Portduino `native-macos` target): + +```bash +cd lobbs-meshtastic-firmware +pio test -e native-macos -f test_lobbs_commands +``` + +Portduino is part of Meshtastic’s native build. LoBBS does not support it as a standalone platform. + +MeshCore firmware builds are verified with `pio run` on a representative env (for example `RAK_4631_terminal_chat`). Bench `/install` and long-reply checks on hardware are manual. diff --git a/ci/host/.gitignore b/ci/host/.gitignore new file mode 100644 index 0000000..aeaebb2 --- /dev/null +++ b/ci/host/.gitignore @@ -0,0 +1 @@ +.pio \ No newline at end of file diff --git a/ci/host/platformio.ini b/ci/host/platformio.ini new file mode 100644 index 0000000..3e33df4 --- /dev/null +++ b/ci/host/platformio.ini @@ -0,0 +1,28 @@ +; Host protocol tests. From repo root: cd ci/host && pio test -e host -f test_protocol_host +[platformio] +src_dir = ../.. +test_dir = test + +[env:host] +platform = native +extra_scripts = ../../platforms/extra_script.py +test_framework = unity +test_build_src = true +build_flags = + -std=c++17 + -DLOBBS_PLATFORM_NATIVE=1 + -DPIO_UNIT_TESTING=1 +build_src_filter = + + + + + + + - + + + + + + + + + + + + + + + + + + diff --git a/ci/host/test/test_protocol_host/test_main.cpp b/ci/host/test/test_protocol_host/test_main.cpp new file mode 100644 index 0000000..3b463d2 --- /dev/null +++ b/ci/host/test/test_protocol_host/test_main.cpp @@ -0,0 +1,14 @@ +#include "tests/protocol_tests.h" +#include + +void setUp(void) {} +void tearDown(void) {} + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + UNITY_BEGIN(); + lobbsRunProtocolTests(); + return UNITY_END(); +} diff --git a/core/LoBBSArch.h b/core/LoBBSArch.h new file mode 100644 index 0000000..8f227f4 --- /dev/null +++ b/core/LoBBSArch.h @@ -0,0 +1,17 @@ +#pragma once + +#if defined(ARCH_NRF52) || defined(NRF52_PLATFORM) || defined(ARDUINO_ARCH_NRF52) +#define LOBBS_ARCH_NRF52 1 +#endif + +#if defined(ARCH_ESP32) || defined(ESP32_PLATFORM) || defined(ESP32) +#define LOBBS_ARCH_ESP32 1 +#endif + +#if defined(ARCH_RP2040) || defined(RP2040_PLATFORM) || defined(RP2040) +#define LOBBS_ARCH_RP2040 1 +#endif + +#if defined(ARCH_PORTDUINO) +#define LOBBS_ARCH_PORTDUINO 1 +#endif diff --git a/core/LoBBSBootTrace.h b/core/LoBBSBootTrace.h new file mode 100644 index 0000000..93b7b96 --- /dev/null +++ b/core/LoBBSBootTrace.h @@ -0,0 +1,5 @@ +#pragma once +#include "platforms/LoPlatform.h" + +/** Log immediately before a boot step; last line wins when diagnosing hangs. */ +#define LOBBS_BOOT_STEP(msg) LOBBS_LOG_INFO("LoBBS> %s", (msg)) diff --git a/LoBBSCommandCtx.h b/core/LoBBSCommandCtx.h similarity index 76% rename from LoBBSCommandCtx.h rename to core/LoBBSCommandCtx.h index 9ff9e40..2045603 100644 --- a/LoBBSCommandCtx.h +++ b/core/LoBBSCommandCtx.h @@ -1,9 +1,9 @@ #pragma once #include "LoBBSConfig.h" +#include "LoWire.h" #include -class LoBBSModule; -typedef struct _meshtastic_MeshPacket meshtastic_MeshPacket; +class LoBBSKernel; /** Resolved once per incoming command in dispatch (Auth owns load rules). */ struct LoBBSSession { @@ -15,12 +15,11 @@ struct LoBBSSession { }; struct LoBBSCommandCtx { - LoBBSModule *mod = nullptr; - const meshtastic_MeshPacket *mp = nullptr; + LoBBSKernel *kernel = nullptr; + LoInbound inbound; uint32_t reqId = 0; LoBBSSession session; /** Remainder after verb; mutable cursor for shift/takePage. */ char *rest = nullptr; uint32_t page = 1; }; - diff --git a/LoBBSCommandRegistry.cpp b/core/LoBBSCommandRegistry.cpp similarity index 95% rename from LoBBSCommandRegistry.cpp rename to core/LoBBSCommandRegistry.cpp index ec7bfbd..e3fe336 100644 --- a/LoBBSCommandRegistry.cpp +++ b/core/LoBBSCommandRegistry.cpp @@ -1,11 +1,11 @@ #include "LoBBSCommandRegistry.h" #include "LoBBSHooks.h" #include "LoBBSInstall.h" -#include "LoBBSModule.h" +#include "LoBBSKernel.h" #include "LoBBSReply.h" #include "LoBBSReplyCache.h" #include "LoBBSResponse.h" -#include "gps/RTC.h" +#include "platforms/LoPlatform.h" #include #include #include @@ -322,15 +322,15 @@ static bool lobbsPeelSlashLine(char *line, LoBBSCommandCtx &ctx, char **verbOut, return true; } -void lobbsCommandsHandle(LoBBSModule *mod, const meshtastic_MeshPacket &mp, const LoBBSSession &session, char *line) +void lobbsCommandsHandle(LoBBSKernel *kernel, LoInbound &inbound, const LoBBSSession &session, char *line) { LoBBSCommandCtx ctx; - ctx.mod = mod; - ctx.mp = ∓ + ctx.kernel = kernel; + ctx.inbound = inbound; ctx.session = session; ctx.page = 1; - lobbsReplyCacheGc(getTime()); + lobbsReplyCacheGc(lobbsPlatformUnixNow()); char *verb = nullptr; char *rest = nullptr; @@ -348,7 +348,7 @@ void lobbsCommandsHandle(LoBBSModule *mod, const meshtastic_MeshPacket &mp, cons return; } - const LoBBSInstallState install = mod->installState(); + const LoBBSInstallState install = kernel->installState(); const bool isHelp = strcasecmp(verb, "help") == 0; const bool isInstall = strcasecmp(verb, "install") == 0; if (install != LoBBSInstallState::Ready && !isHelp && !isInstall) { @@ -356,11 +356,11 @@ void lobbsCommandsHandle(LoBBSModule *mod, const meshtastic_MeshPacket &mp, cons char mounts[48]; lobbsInstallMountList(ctx, mounts, sizeof(mounts)); char msg[160]; - snprintf(msg, sizeof(msg), "New LoBBS. Sysop: /install <%s> ", mounts[0] ? mounts : "flash"); + snprintf(msg, sizeof(msg), "New LoBBS. Sysop: /install <%s> ", mounts[0] ? mounts : "internal"); lobbsCommandReplyError(ctx, msg); } else { char msg[96]; - snprintf(msg, sizeof(msg), "LoBBS offline: cannot open %s.", lobbsInstallOfflineHome(*mod)); + snprintf(msg, sizeof(msg), "LoBBS offline: cannot open %s.", lobbsInstallOfflineHome(*kernel)); lobbsCommandReplyError(ctx, msg); } return; diff --git a/LoBBSCommandRegistry.h b/core/LoBBSCommandRegistry.h similarity index 89% rename from LoBBSCommandRegistry.h rename to core/LoBBSCommandRegistry.h index af3be73..35812f2 100644 --- a/LoBBSCommandRegistry.h +++ b/core/LoBBSCommandRegistry.h @@ -4,8 +4,7 @@ #include #include -class LoBBSModule; -typedef struct _meshtastic_MeshPacket meshtastic_MeshPacket; +class LoBBSKernel; static constexpr uint8_t LOBBS_V_LOGIN = 1; static constexpr uint8_t LOBBS_V_SYSOP = 2; @@ -41,5 +40,5 @@ void lobbsHelpForTable(const LoBBSCommandCtx *ctx, LoScalar &value, const LoScal bool lobbsDispatchSub(LoBBSCommandCtx &ctx, const LoBBSVerb *table, size_t count); int lobbsArgShiftMany(LoBBSCommandCtx &ctx, const char *out[], int maxOut); -void lobbsCommandsHandle(LoBBSModule *mod, const meshtastic_MeshPacket &mp, const LoBBSSession &session, char *line); +void lobbsCommandsHandle(LoBBSKernel *kernel, LoInbound &inbound, const LoBBSSession &session, char *line); diff --git a/LoBBSConfig.h b/core/LoBBSConfig.h similarity index 76% rename from LoBBSConfig.h rename to core/LoBBSConfig.h index 024bb86..46e1de8 100644 --- a/LoBBSConfig.h +++ b/core/LoBBSConfig.h @@ -1,4 +1,6 @@ #pragma once +#include "LoBBSArch.h" + #define LOBBS_MAX_USERNAME_LEN 32 #define LOBBS_USERNAME_BUFFER_SIZE (LOBBS_MAX_USERNAME_LEN + 1) @@ -22,6 +24,21 @@ #define LOBBS_EXTRA_QSPI 0 #endif +#ifndef LOFS_ENABLE_EXTRA_QSPI +#define LOFS_ENABLE_EXTRA_QSPI LOBBS_EXTRA_QSPI +#endif + +#if LOBBS_ARCH_NRF52 +#define LOFS_NRF52 1 +#ifndef LOBBS_NRF52_FLASH_PAGE_SIZE +#ifdef FLASH_NRF52_PAGE_SIZE +#define LOBBS_NRF52_FLASH_PAGE_SIZE FLASH_NRF52_PAGE_SIZE +#else +#define LOBBS_NRF52_FLASH_PAGE_SIZE 4096 +#endif +#endif +#endif + #if defined(LOBBS_DEMO_MODE) || defined(PIO_UNIT_TESTING) #define LOBBS_SEED 1 #endif diff --git a/core/LoBBSDispatch.cpp b/core/LoBBSDispatch.cpp new file mode 100644 index 0000000..effde0c --- /dev/null +++ b/core/LoBBSDispatch.cpp @@ -0,0 +1,90 @@ +#include "LoBBSDispatch.h" +#include "LoBBSCommandRegistry.h" +#include "LoBBSKernel.h" +#include "platforms/LoPlatform.h" +#include "apps/Auth/AuthDal.h" +#include + +#include "LoBBSStackGuard.h" +#include "apps/AppUtil.h" +#include "apps/Mail/MailDal.h" +#include "LoBBSConfig.h" + +static void lobbsTrimLine(char *line) +{ + if (!line) + return; + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == ' ' || line[len - 1] == '\t' || line[len - 1] == '\r' || line[len - 1] == '\n')) + line[--len] = '\0'; + char *start = line; + while (*start == ' ' || *start == '\t') + start++; + if (start != line) + memmove(line, start, strlen(start) + 1); +} + +static LoBBSSession lobbsResolveSession(LoBBSKernel *kernel, uint32_t wireNodeId) +{ + LoBBSSession session; + session.nodeId = wireNodeId; + AuthDal &auth = kernel->auth().dal(); + LoScalar userRow; + uint32_t sessionNodeId = wireNodeId; + uint64_t authUserUuid = 0; + std::string cwd; + if (!auth.loadUserByNodeId(wireNodeId, &userRow, &sessionNodeId, &authUserUuid, &cwd)) + return session; + session.nodeId = sessionNodeId; + session.userUuid = authUserUuid; + if (!cwd.empty() && cwd[0] == '/' && cwd.size() < sizeof(session.cwd)) + memcpy(session.cwd, cwd.c_str(), cwd.size() + 1); + if (!AuthDal::userUsername(userRow, session.username, sizeof(session.username))) + session.username[0] = '\0'; + session.isSysop = AuthDal::userIsSysop(userRow); + return session; +} + +void lobbsCoreHandleInbound(LoBBSKernel *kernel, LoInbound &in) +{ + if (!kernel || !in.text) + return; + + char *line = kernel->msgBuffer; + size_t copyLen = strlen(in.text); + if (copyLen >= sizeof(kernel->msgBuffer)) + copyLen = sizeof(kernel->msgBuffer) - 1; + memcpy(line, in.text, copyLen); + line[copyLen] = '\0'; + + lobbsTrimLine(line); + if (line[0] == '\0' || line[0] != '/') + return; + + const LoBBSSession session = lobbsResolveSession(kernel, in.from.key); + lobbsCommandsHandle(kernel, in, session, line); + lobbsPlatformNoteHandlerStack(); +} + +void lobbsCoreDeliverUserMail(LoBBSKernel *kernel, LoInbound &in) +{ + if (!kernel || !in.text || !in.text[0] || in.text[0] == '/') + return; + + uint64_t sysop = lobbsAppUuidForUsername(kernel, "sysop"); + if (!sysop) + return; + + uint64_t fromUuid = 0; + LoScalar user; + (void)kernel->auth().dal().loadUserByNodeId(in.from.key, &user, nullptr, &fromUuid, nullptr); + + char body[LOBBS_MESSAGE_BODY_BUFFER_SIZE]; + strncpy(body, in.text, LOBBS_MESSAGE_BODY_MAX); + body[LOBBS_MESSAGE_BODY_MAX] = '\0'; + + if (kernel->mail().dal().sendMail(fromUuid, sysop, body) == LODB_OK) + kernel->sendReply(in, "Message sent."); + else + kernel->sendReply(in, "Mailbox unavailable."); +} diff --git a/core/LoBBSDispatch.h b/core/LoBBSDispatch.h new file mode 100644 index 0000000..ebd4096 --- /dev/null +++ b/core/LoBBSDispatch.h @@ -0,0 +1,8 @@ +#pragma once +#include "LoWire.h" + +class LoBBSKernel; + +void lobbsCoreHandleInbound(LoBBSKernel *kernel, LoInbound &in); +/** Non-command user text (e.g. DM body) to sysop mail. */ +void lobbsCoreDeliverUserMail(LoBBSKernel *kernel, LoInbound &in); diff --git a/LoBBSHooks.cpp b/core/LoBBSHooks.cpp similarity index 86% rename from LoBBSHooks.cpp rename to core/LoBBSHooks.cpp index 7d06d9c..e24176a 100644 --- a/LoBBSHooks.cpp +++ b/core/LoBBSHooks.cpp @@ -1,5 +1,5 @@ #include "LoBBSHooks.h" -#include "configuration.h" +#include "platforms/LoPlatform.h" #include #include "LoBBSStackGuard.h" @@ -24,7 +24,11 @@ static void lobbsAddHook(const char *name, LobbsHookKind kind, void (*fn)(), int { if (!name || !fn) return; - LobbsHookEntry entry{name, kind, fn, priority}; + LobbsHookEntry entry; + entry.name = name; + entry.kind = kind; + entry.fn = fn; + entry.priority = priority; size_t insertAt = lobbsHooks.size(); for (size_t i = 0; i < lobbsHooks.size(); i++) { if (priority < lobbsHooks[i].priority) { @@ -58,7 +62,7 @@ void lobbsDoAction(const char *name, LoBBSCommandCtx &ctx, const LoScalar &args) if (!lobbsHooks[i].name || strcasecmp(lobbsHooks[i].name, name) != 0) continue; if (lobbsHooks[i].kind != LobbsHookKind::Action) { - LOG_ERROR("LoBBS hook %s: handler registered with the wrong kind", name); + LOBBS_LOG_ERROR("LoBBS hook %s: handler registered with the wrong kind", name); continue; } reinterpret_cast(lobbsHooks[i].fn)(&ctx, args); @@ -73,7 +77,7 @@ void lobbsApplyFilter(const char *name, LoBBSCommandCtx &ctx, LoScalar &value, c if (!lobbsHooks[i].name || strcasecmp(lobbsHooks[i].name, name) != 0) continue; if (lobbsHooks[i].kind != LobbsHookKind::Record) { - LOG_ERROR("LoBBS hook %s: handler registered with the wrong kind", name); + LOBBS_LOG_ERROR("LoBBS hook %s: handler registered with the wrong kind", name); continue; } reinterpret_cast(lobbsHooks[i].fn)(&ctx, value, args); @@ -88,7 +92,7 @@ void lobbsApplyFilter(const char *name, LoBBSCommandCtx &ctx, std::vector(lobbsHooks[i].fn)(&ctx, value, args); diff --git a/LoBBSHooks.h b/core/LoBBSHooks.h similarity index 100% rename from LoBBSHooks.h rename to core/LoBBSHooks.h diff --git a/LoBBSInstall.cpp b/core/LoBBSInstall.cpp similarity index 75% rename from LoBBSInstall.cpp rename to core/LoBBSInstall.cpp index 5e3ac68..d604006 100644 --- a/LoBBSInstall.cpp +++ b/core/LoBBSInstall.cpp @@ -2,12 +2,10 @@ #include "LoBBSCommandRegistry.h" #include "LoBBSConfig.h" #include "LoBBSHooks.h" -#include "LoBBSModule.h" +#include "LoBBSKernel.h" #include "apps/AppUtil.h" #include "apps/Auth/AuthDal.h" -#include "configuration.h" -#include "mesh/NodeDB.h" -#include "mesh/Router.h" +#include "platforms/LoPlatform.h" #include #include #include @@ -21,21 +19,21 @@ static LoBBSInstallState gInstallState = LoBBSInstallState::Blank; static char gInstallRoot[32] = {0}; static char gOfflineHome[32] = {0}; -LoBBSInstallState lobbsInstallState(const LoBBSModule &mod) +LoBBSInstallState lobbsInstallState(const LoBBSKernel &kernel) { - (void)mod; + (void)kernel; return gInstallState; } -const char *lobbsInstallRoot(const LoBBSModule &mod) +const char *lobbsInstallRoot(const LoBBSKernel &kernel) { - (void)mod; + (void)kernel; return gInstallRoot; } -const char *lobbsInstallOfflineHome(const LoBBSModule &mod) +const char *lobbsInstallOfflineHome(const LoBBSKernel &kernel) { - (void)mod; + (void)kernel; return gOfflineHome; } @@ -103,7 +101,7 @@ bool lobbsInstallWriteMarker(const char *root) if (!rec.encode(line, 256)) return false; - File f = LoFS::open(path, FILE_O_WRITE); + LoFile f = LoFS::open(path, "w"); if (!f) return false; size_t w = f.write((const uint8_t *)line.data(), line.size()); @@ -112,21 +110,21 @@ bool lobbsInstallWriteMarker(const char *root) return w == line.size(); } -static bool lobbsOpenHome(LoBBSModule &mod, const char *root) +static bool lobbsOpenHome(LoBBSKernel &kernel, const char *root) { char home[32]; - LoDb *db = mod.lodb(); + LoDb *db = kernel.lodb(); return db && lobbsInstallHome(root, home, sizeof(home)) && db->open(home) == LODB_OK; } -void lobbsInstallInit(LoBBSModule &mod) +void lobbsInstallInit(LoBBSKernel &kernel) { gInstallState = LoBBSInstallState::Blank; gInstallRoot[0] = '\0'; gOfflineHome[0] = '\0'; LoBBSCommandCtx ctx; - ctx.mod = &mod; + ctx.kernel = &kernel; char root[16] = {0}; std::string name; for (const LoScalar &rec : lobbsInstallMounts(ctx)) { @@ -137,49 +135,32 @@ void lobbsInstallInit(LoBBSModule &mod) if (!lobbsInstallMarkerPath(candidate, marker, sizeof(marker)) || !LoFS::exists(marker)) continue; if (root[0]) - LOG_WARN("LoBBS: ignoring second install at %s (using %s)", candidate, root); + LOBBS_LOG_WARN("LoBBS: ignoring second install at %s (using %s)", candidate, root); else strncpy(root, candidate, sizeof(root) - 1); } if (!root[0]) { - LOG_INFO("LoBBS: blank (no install found)"); + LOBBS_LOG_INFO("LoBBS: blank (no install found)"); return; } - if (!lobbsOpenHome(mod, root)) { + if (!lobbsOpenHome(kernel, root)) { gInstallState = LoBBSInstallState::Offline; lobbsInstallHome(root, gOfflineHome, sizeof(gOfflineHome)); - LOG_ERROR("LoBBS offline: failed to open database at %s", gOfflineHome); + LOBBS_LOG_ERROR("LoBBS offline: failed to open database at %s", gOfflineHome); return; } strncpy(gInstallRoot, root, sizeof(gInstallRoot) - 1); gInstallState = LoBBSInstallState::Ready; - lobbsInstallDatabaseOpened(mod); - LOG_INFO("LoBBS ready at %s", root); + lobbsInstallDatabaseOpened(kernel); + LOBBS_LOG_INFO("LoBBS ready at %s", root); } -void lobbsInstallDatabaseOpened(LoBBSModule &mod) +void lobbsInstallDatabaseOpened(LoBBSKernel &kernel) { - mod.auth().dal().clearSessions(); - mod.config().dal().notifyDatabaseOpened(mod); -} - -bool lobbsInstallAuthorized(const meshtastic_MeshPacket &mp) -{ - if (mp.from == 0) - return true; -#if !MESHTASTIC_EXCLUDE_PKI - if (mp.pki_encrypted) { - if ((config.security.admin_key[0].size == 32 && - memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || - (config.security.admin_key[1].size == 32 && - memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || - (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) - return true; - } -#endif - return false; + kernel.auth().dal().clearSessions(); + kernel.config().dal().notifyDatabaseOpened(kernel); } static bool lobbsInstallLocToRoot(LoBBSCommandCtx &ctx, const char *loc, char *rootOut, size_t cap) @@ -204,7 +185,7 @@ static void handleInstall(LoBBSCommandCtx &ctx) lobbsCommandReplyError(ctx, msg); return; } - if (!lobbsInstallAuthorized(*ctx.mp)) { + if (!lobbsPlatformInstallAuthorized(ctx.inbound)) { lobbsCommandReplyError(ctx, "Not authorized."); return; } @@ -227,13 +208,13 @@ static void handleInstall(LoBBSCommandCtx &ctx) return; } - if (!lobbsOpenHome(*ctx.mod, root)) { + if (!lobbsOpenHome(*ctx.kernel, root)) { lobbsCommandReplyError(ctx, "Failed to open database."); return; } - AuthDal &auth = ctx.mod->auth().dal(); - lobbsInstallDatabaseOpened(*ctx.mod); + AuthDal &auth = ctx.kernel->auth().dal(); + lobbsInstallDatabaseOpened(*ctx.kernel); const bool hasUsers = auth.countAllUsers() > 0; if (hasUsers) { LoScalar row; @@ -241,8 +222,8 @@ static void handleInstall(LoBBSCommandCtx &ctx) lobbsCommandReplyError(ctx, "Existing database: sysop credentials required."); return; } - auth.loginUser(user, getFrom(ctx.mp)); - } else if (LoDbError err = auth.createUser(user, pass, getFrom(ctx.mp), true)) { + auth.loginUser(user, ctx.inbound.from.key); + } else if (LoDbError err = auth.createUser(user, pass, ctx.inbound.from.key, true)) { lobbsCommandReplyError(ctx, lobbsDbErrorText(err, "Failed to create sysop.")); return; } @@ -274,7 +255,7 @@ void lobbsInstallRegisterCommands() } #if LOBBS_SEED -void lobbsInstallAutoSeed(LoBBSModule &mod) +void lobbsInstallAutoSeed(LoBBSKernel &kernel) { LOBBS_BOOT_STEP("autoseed: enter"); gInstallState = LoBBSInstallState::Blank; @@ -282,30 +263,30 @@ void lobbsInstallAutoSeed(LoBBSModule &mod) gOfflineHome[0] = '\0'; LoBBSCommandCtx ctx; - ctx.mod = &mod; + ctx.kernel = &kernel; LOBBS_BOOT_STEP("autoseed: install_mounts"); std::vector mounts = lobbsInstallMounts(ctx); std::string name; if (mounts.empty() || !mounts[0].getString(LODB_F_TITLE, name)) { - LOG_ERROR("LoBBS seed: no install mount"); + LOBBS_LOG_ERROR("LoBBS seed: no install mount"); return; } char root[16]; snprintf(root, sizeof(root), "/%s", name.c_str()); char home[32]; lobbsInstallHome(root, home, sizeof(home)); - LOG_INFO("LoBBS> autoseed: rmdir %s", home); + LOBBS_LOG_INFO("LoBBS> autoseed: rmdir %s", home); LoFS::rmdir(home, true); - LOG_INFO("LoBBS> autoseed: lodb open %s", home); - if (!lobbsOpenHome(mod, root) || !lobbsInstallWriteMarker(root)) { - LOG_ERROR("LoBBS seed install failed at %s", root); + LOBBS_LOG_INFO("LoBBS> autoseed: lodb open %s", home); + if (!lobbsOpenHome(kernel, root) || !lobbsInstallWriteMarker(root)) { + LOBBS_LOG_ERROR("LoBBS seed install failed at %s", root); return; } LOBBS_BOOT_STEP("autoseed: lobbsInstallDatabaseOpened"); - lobbsInstallDatabaseOpened(mod); + lobbsInstallDatabaseOpened(kernel); strncpy(gInstallRoot, root, sizeof(gInstallRoot) - 1); gInstallState = LoBBSInstallState::Ready; - LOG_INFO("LoBBS seed install at %s", root); + LOBBS_LOG_INFO("LoBBS seed install at %s", root); } #endif diff --git a/LoBBSInstall.h b/core/LoBBSInstall.h similarity index 65% rename from LoBBSInstall.h rename to core/LoBBSInstall.h index 4e766a7..caac0db 100644 --- a/LoBBSInstall.h +++ b/core/LoBBSInstall.h @@ -3,9 +3,8 @@ #include #include -class LoBBSModule; +class LoBBSKernel; struct LoBBSCommandCtx; -typedef struct _meshtastic_MeshPacket meshtastic_MeshPacket; enum class LoBBSInstallState : uint8_t { Blank, Ready, Offline }; @@ -14,25 +13,24 @@ static constexpr const char *LOBBS_HOME_DIR = "lobbs"; static constexpr const char *LOBBS_INSTALL_MARKER_NAME = "install.ls"; static constexpr uint32_t LOBBS_INSTALL_FIELD_VERSION = 0; -LoBBSInstallState lobbsInstallState(const LoBBSModule &mod); +LoBBSInstallState lobbsInstallState(const LoBBSKernel &kernel); /** Mount root of the install (e.g. `/extra`), empty when not Ready. */ -const char *lobbsInstallRoot(const LoBBSModule &mod); +const char *lobbsInstallRoot(const LoBBSKernel &kernel); /** LoBBS home that failed to open while Offline (e.g. `/extra/lobbs`). */ -const char *lobbsInstallOfflineHome(const LoBBSModule &mod); +const char *lobbsInstallOfflineHome(const LoBBSKernel &kernel); -void lobbsInstallInit(LoBBSModule &mod); -void lobbsInstallDatabaseOpened(LoBBSModule &mod); -/** `install_mounts` names joined with `|`, e.g. `sd|flash2`. */ +void lobbsInstallInit(LoBBSKernel &kernel); +void lobbsInstallDatabaseOpened(LoBBSKernel &kernel); +/** `install_mounts` names joined with `|`, e.g. `sd|qspi|reserve|spare|internal`. */ void lobbsInstallMountList(LoBBSCommandCtx &ctx, char *out, size_t cap); -bool lobbsInstallAuthorized(const meshtastic_MeshPacket &mp); /** `/lobbs`, e.g. `/extra/lobbs`. */ bool lobbsInstallHome(const char *root, char *out, size_t cap); /** Writes `/lobbs/install.ls`; its presence marks the install. */ bool lobbsInstallWriteMarker(const char *root); #if LOBBS_SEED -void lobbsInstallAutoSeed(LoBBSModule &mod); +void lobbsInstallAutoSeed(LoBBSKernel &kernel); #endif void lobbsInstallRegisterCommands(); diff --git a/core/LoBBSKernel.cpp b/core/LoBBSKernel.cpp new file mode 100644 index 0000000..d90d1d5 --- /dev/null +++ b/core/LoBBSKernel.cpp @@ -0,0 +1,81 @@ +#include "LoBBSKernel.h" +#include "platforms/LoPlatform.h" +#include "LoBBSConfig.h" +#include "LoBBSWireup.h" +#include "LoBBSInstall.h" +#include +#if LOBBS_SEED +#include "LoBBSSeed.h" +#endif +#include "LoBBSReply.h" +#include +#include + +#include "LoBBSBootTrace.h" +#include "LoBBSStackGuard.h" + +LoBBSKernel::LoBBSKernel() + : lodb_(new LoDb("db")), config_(*lodb_), auth_(*lodb_), mail_(*lodb_), news_(*lodb_), yarn_(*lodb_), wall_(*lodb_) +{ +} + +void LoBBSKernel::begin() +{ + if (begun_) + return; + begun_ = true; + LOBBS_BOOT_STEP("core begin"); + LOBBS_BOOT_STEP("LoFS::begin"); + LoFS::begin(); + LOBBS_BOOT_STEP("lobbsWireup"); + lobbsWireup(); +#ifdef LOBBS_DEMO_MODE + LOBBS_BOOT_STEP("lobbsInstallAutoSeed"); + lobbsInstallAutoSeed(*this); + LOBBS_BOOT_STEP("lobbsSeedAll"); + lobbsSeedAll(*this); +#else + LOBBS_BOOT_STEP("lobbsInstallInit"); + lobbsInstallInit(*this); +#endif + LOBBS_BOOT_STEP("core begin done"); +} + +LoBBSInstallState LoBBSKernel::installState() const +{ + return lobbsInstallState(*this); +} + +LoBBSKernel::~LoBBSKernel() +{ + delete lodb_; +} + +#ifdef PIO_UNIT_TESTING +#include +std::vector *lobbsTestReplySink = nullptr; +#endif + +void LoBBSKernel::sendReply(const LoInbound &req, const char *msg) +{ + if (!msg) + return; +#ifdef PIO_UNIT_TESTING + if (lobbsTestReplySink) { + lobbsTestReplySink->push_back(msg); + return; + } +#endif + char mark[40]; + snprintf(mark, sizeof(mark), "reply alloc %uB", (unsigned)strlen(msg)); + lobbsBreadcrumb(mark); + lobbsPlatformSendReply(req, msg); + lobbsBreadcrumb("reply out"); +} + +void lobbsBreadcrumb(const char *step) +{ + if (!step) + return; + LOBBS_LOG_INFO("LoBBS %s", step); +} diff --git a/LoBBSModule.h b/core/LoBBSKernel.h similarity index 55% rename from LoBBSModule.h rename to core/LoBBSKernel.h index 546d4c2..44f37f1 100644 --- a/LoBBSModule.h +++ b/core/LoBBSKernel.h @@ -1,8 +1,7 @@ #pragma once #include "LoBBSInstall.h" #include "LoBBSVersion.h" -#include "MeshModule.h" -#include "SinglePortModule.h" +#include "LoWire.h" #include "apps/Auth/Auth.h" #include "apps/Config/Config.h" #include "apps/Mail/Mail.h" @@ -12,20 +11,21 @@ #include #include -class LoBBSModule; +class LoBBSKernel; void lobbsBreadcrumb(const char *step); -class LoBBSModule : public SinglePortModule +class LoBBSKernel { - friend ProcessMessage lobbsDispatchReceived(class LoBBSModule *mod, const meshtastic_MeshPacket &mp); - public: - LoBBSModule(); - ~LoBBSModule(); + LoBBSKernel(); + ~LoBBSKernel(); - void sendReply(const meshtastic_MeshPacket &req, const char *msg); - void sendReply(const meshtastic_MeshPacket &req, const std::string &msg) { sendReply(req, msg.c_str()); } + /** Host binds FS (MeshCore) then calls once before inbound traffic. */ + void begin(); + + void sendReply(const LoInbound &req, const char *msg); + void sendReply(const LoInbound &req, const std::string &msg) { sendReply(req, msg.c_str()); } ConfigApp &config() { return config_; } AuthApp &auth() { return auth_; } @@ -36,23 +36,15 @@ class LoBBSModule : public SinglePortModule LoDb *lodb() { return lodb_; } LoBBSInstallState installState() const; - protected: - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; char msgBuffer[256]; private: + bool begun_ = false; LoDb *lodb_; ConfigApp config_; AuthApp auth_; MailApp mail_; NewsApp news_; - WallApp wall_; YarnApp yarn_; + WallApp wall_; }; - -#ifdef PIO_UNIT_TESTING -#include -#include -extern std::vector *lobbsTestReplySink; -#endif - diff --git a/LoBBSReply.h b/core/LoBBSReply.h similarity index 100% rename from LoBBSReply.h rename to core/LoBBSReply.h diff --git a/LoBBSReplyCache.cpp b/core/LoBBSReplyCache.cpp similarity index 93% rename from LoBBSReplyCache.cpp rename to core/LoBBSReplyCache.cpp index 6103dc7..c2d6338 100644 --- a/LoBBSReplyCache.cpp +++ b/core/LoBBSReplyCache.cpp @@ -1,6 +1,6 @@ #include "LoBBSReplyCache.h" #include "apps/Config/ConfigCommands.h" -#include "gps/RTC.h" +#include "platforms/LoPlatform.h" #include #include #include @@ -55,7 +55,7 @@ bool lobbsReplyCacheStore(uint32_t sessionNodeId, const LoBBSResponse &resp) LobbsReplyCacheEntry entry; entry.resp = resp; - entry.expiresSec = getTime() + lobbsReplyCacheTtlSec(); + entry.expiresSec = lobbsPlatformUnixNow() + lobbsReplyCacheTtlSec(); lobbsReplyCacheBySession[sessionNodeId] = std::move(entry); while (lobbsReplyCacheBySession.size() > LOBBS_REPLY_CACHE_MAX_ENTRIES) { lobbsReplyCacheBySession.erase(lobbsReplyCacheBySession.begin()); @@ -68,7 +68,7 @@ bool lobbsReplyCacheLoad(uint32_t sessionNodeId, LoBBSResponse &out) auto it = lobbsReplyCacheBySession.find(sessionNodeId); if (it == lobbsReplyCacheBySession.end()) return false; - uint32_t now = getTime(); + uint32_t now = lobbsPlatformUnixNow(); if (now > it->second.expiresSec) { lobbsReplyCacheBySession.erase(it); return false; diff --git a/LoBBSReplyCache.h b/core/LoBBSReplyCache.h similarity index 100% rename from LoBBSReplyCache.h rename to core/LoBBSReplyCache.h diff --git a/LoBBSResponse.cpp b/core/LoBBSResponse.cpp similarity index 88% rename from LoBBSResponse.cpp rename to core/LoBBSResponse.cpp index 272699b..dd7276a 100644 --- a/LoBBSResponse.cpp +++ b/core/LoBBSResponse.cpp @@ -1,8 +1,7 @@ #include "LoBBSResponse.h" -#include "LoBBSModule.h" +#include "LoBBSKernel.h" #include "LoBBSReply.h" #include "LoBBSReplyCache.h" -#include "gps/RTC.h" #include "protocol/machine/paginate.h" #include "protocol/machine/serialize.h" #include "protocol/plain_text/paginate.h" @@ -36,17 +35,17 @@ void lobbsResponseAppendRecord(LoBBSResponse &resp, const LoScalar &record) } static void lobbsSendSentence(LoBBSCommandCtx &ctx, const char *msg) { - if (!ctx.mod || !ctx.mp) + if (!ctx.kernel) return; if (!msg) msg = ""; if (ctx.reqId == 0) { - ctx.mod->sendReply(*ctx.mp, msg); + ctx.kernel->sendReply(ctx.inbound, msg); return; } char buf[LOBBS_REPLY_BYTES + 32]; snprintf(buf, sizeof(buf), "<%u>%s", ctx.reqId, msg); - ctx.mod->sendReply(*ctx.mp, buf); + ctx.kernel->sendReply(ctx.inbound, buf); } static bool lobbsRenderPage(LoBBSCommandCtx &ctx, const LoBBSResponse &resp) @@ -64,7 +63,7 @@ static bool lobbsRenderPage(LoBBSCommandCtx &ctx, const LoBBSResponse &resp) lobbsSendSentence(ctx, errMsg ? errMsg : "Empty."); return false; } - ctx.mod->sendReply(*ctx.mp, pageText.c_str()); + ctx.kernel->sendReply(ctx.inbound, pageText.c_str()); return true; } @@ -77,7 +76,7 @@ static bool lobbsRenderPage(LoBBSCommandCtx &ctx, const LoBBSResponse &resp) lobbsSendSentence(ctx, errMsg ? errMsg : "Empty."); return false; } - ctx.mod->sendReply(*ctx.mp, pageText.c_str()); + ctx.kernel->sendReply(ctx.inbound, pageText.c_str()); return true; } @@ -89,7 +88,7 @@ void lobbsCommandReplyError(LoBBSCommandCtx &ctx, const char *message) void lobbsReplySendCachedPage(LoBBSCommandCtx &ctx) { - if (!ctx.mod || !ctx.mp) + if (!ctx.kernel) return; LoBBSResponse resp; if (!lobbsReplyCacheLoad(ctx.session.nodeId, resp)) { @@ -101,11 +100,10 @@ void lobbsReplySendCachedPage(LoBBSCommandCtx &ctx) void lobbsCommandReplyResponse(LoBBSCommandCtx &ctx, const LoBBSResponse &resp) { - if (!ctx.mod || !ctx.mp) + if (!ctx.kernel) return; if (!resp.ok) { - lobbsReplyCacheErase(ctx.session.nodeId); lobbsSendSentence(ctx, resp.error.c_str()); return; } diff --git a/LoBBSResponse.h b/core/LoBBSResponse.h similarity index 100% rename from LoBBSResponse.h rename to core/LoBBSResponse.h diff --git a/LoBBSSeed.cpp b/core/LoBBSSeed.cpp similarity index 79% rename from LoBBSSeed.cpp rename to core/LoBBSSeed.cpp index 2dd1335..0d6f847 100644 --- a/LoBBSSeed.cpp +++ b/core/LoBBSSeed.cpp @@ -3,17 +3,17 @@ #include "LoBBSCommandCtx.h" #include "LoBBSHooks.h" -#include "LoBBSModule.h" +#include "LoBBSKernel.h" #include "LoBBSSeed.h" #include "LoBBSBootTrace.h" #include "LoBBSStackGuard.h" -void lobbsSeedAll(LoBBSModule &mod) +void lobbsSeedAll(LoBBSKernel &kernel) { LOBBS_BOOT_STEP("seed all: lobbsDoAction seed"); LoBBSCommandCtx ctx; - ctx.mod = &mod; + ctx.kernel = &kernel; lobbsDoAction("seed", ctx, LoScalar()); LOBBS_BOOT_STEP("seed all: done"); } diff --git a/core/LoBBSSeed.h b/core/LoBBSSeed.h new file mode 100644 index 0000000..f448bf3 --- /dev/null +++ b/core/LoBBSSeed.h @@ -0,0 +1,7 @@ +#pragma once +#if LOBBS_SEED + +class LoBBSKernel; +void lobbsSeedAll(LoBBSKernel &kernel); + +#endif diff --git a/LoBBSStackGuard.h b/core/LoBBSStackGuard.h similarity index 76% rename from LoBBSStackGuard.h rename to core/LoBBSStackGuard.h index 7df6e6b..171df31 100644 --- a/LoBBSStackGuard.h +++ b/core/LoBBSStackGuard.h @@ -1,6 +1,7 @@ #pragma once // Include after the other includes in every LoBBS .cpp. Commands run on the nRF52 loop task, whose 4 KB stack // is shared with the router and has no overflow trap, so each LoBBS function is capped at compile time. -#if defined(ARDUINO_ARCH_NRF52) && defined(__GNUC__) && !defined(__clang__) +#include "LoBBSArch.h" +#if LOBBS_ARCH_NRF52 && defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic error "-Wstack-usage=512" #endif diff --git a/LoBBSVersion.h b/core/LoBBSVersion.h similarity index 84% rename from LoBBSVersion.h rename to core/LoBBSVersion.h index bd5789d..474a5ab 100644 --- a/LoBBSVersion.h +++ b/core/LoBBSVersion.h @@ -1,6 +1,6 @@ #pragma once -// LoBBS semver comes from version.properties [LOBBS], injected like APP_VERSION in bin/platformio-custom.py +// LoBBS semver comes from version.properties [LOBBS], injected by platforms/extra_script.py on the plugin lib. #ifndef LOBBS_VERSION_SHORT #error LOBBS_VERSION_SHORT must be set by the build environment diff --git a/LoBBSWireup.cpp b/core/LoBBSWireup.cpp similarity index 100% rename from LoBBSWireup.cpp rename to core/LoBBSWireup.cpp diff --git a/LoBBSWireup.h b/core/LoBBSWireup.h similarity index 100% rename from LoBBSWireup.h rename to core/LoBBSWireup.h diff --git a/core/LoWire.h b/core/LoWire.h new file mode 100644 index 0000000..70b5b35 --- /dev/null +++ b/core/LoWire.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +/** Session and reply routing key. Meshtastic uses node num; MeshCore uses a stable hash of the contact key. */ +struct LoNodeId { + uint32_t key = 0; +}; + +/** Local = trusted operator path (USB console on MeshCore, phone/API client on Meshtastic). */ +enum class LoInboundChannel : uint8_t { Radio = 0, Local = 1 }; + +/** One inbound text line or DM payload. */ +struct LoInbound { + LoNodeId from; + LoInboundChannel channel = LoInboundChannel::Radio; + const char *text = nullptr; + /** Platform-owned reply context (Meshtastic MeshPacket, MeshCore ContactInfo, etc.). */ + const void *replyContext = nullptr; +}; diff --git a/docs/lobbs-internals.md b/docs/lobbs-internals.md index 17e9704..c5cd1e3 100644 --- a/docs/lobbs-internals.md +++ b/docs/lobbs-internals.md @@ -95,19 +95,25 @@ Machine: records encoded as LoScalar lines, sliced into pages with `ok [n:ma ## LoFS and install -LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `//…` (optional `sd`, optional `extra` when built with `LOBBS_EXTRA_QSPI=1`, `flash2` on nRF52840, `flash`). 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 `//…`. 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. -`/flash2` is a second nRF52840 internal-flash partition, 100 KiB at `0xD4000`–`0xED000` between the app and InternalFS. The linker scripts `src/platform/nrf52/nrf52840_s140_v6.ld` (set on `nrf52840_base`) and `nrf52840_s140_v7.ld` end `FLASH` at `0xD4000` and provide `__flash2_start` / `__flash2_end`. LoFS reads them as weak symbols, so nRF builds on other linker scripts have no `/flash2`. The partition is a second `Adafruit_LittleFS` with 128 B blocks over `flash_nrf5x`. It is formatted when mount fails or by SysOp `/format flash2` (`LoFS::format`, which erases the pages first). Its block IO takes InternalFS's lock because the `flash_nrf5x` page cache is shared and the BLE task writes bonds to InternalFS. +`/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. -`/extra` is optional onboard QSPI flash (2 MiB, 4096 B sectors, 256 B read/prog, 512-block lookahead). LoFS uses `LoFSQspi` (`nrfx_qspi`, FASTREAD/PP at SCK/8, no quad-enable). Boot sends release-from-deep-power-down (`0xAB`) and software reset (`0x66`/`0x99`), then reads the JEDEC ID and expects Puya `0x85`. A wrong or missing ID skips the mount and never formats. A mount failure on a good ID formats, as does `/format extra`. `/format flash` runs `FSCom.format()` then `nodeDB->saveToDisk()`, the same recovery Meshtastic uses after a failed save. Formatting the install mount reruns `lobbsInstallInit`, so the node goes Blank unless another mount holds an install. Every custom instruction holds IO2/IO3 high: they are WP#/HOLD# while the quad-enable bit is clear, so LoFSQspi polls the status register (`0x05`) itself instead of `nrfx_qspi_mem_busy_check`, which sends it with HOLD# low. +`/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. + +`/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. + +`/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`. `LoFS::list` callbacks get `(ctx, basename, isDirectory, size)`. `size` is the file length, 0 for directories and mounts. -Each mount has a `dbSafe` flag (`LoFS::mountDbSafe`). `sd`, `extra`, and `flash2` are db-safe. `flash` is db-safe only when there is no `flash2` mount, because a failed Meshtastic save on nRF52 formats InternalFS. Install points come from the `install_mounts` list filter. Core registers a provider that lists present db-safe mounts in mount-table order, which is preference order (`sd`, `extra`, `flash2`, `flash`). `/install` accepts only listed mounts, the install hint and usage show them, and demo seeding uses the first one. +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. -Shared mounts (`/flash`) keep `LOFS_SHARED_RESERVE_BYTES` free (16 KiB nRF52, 128 KiB other hardware, 0 Portduino; override with `-D`). `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. +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`. -LoBBS home: everything lives under `//lobbs` (`LOBBS_HOME_DIR`). `install.ls` (field `LOBBS_INSTALL_FIELD_VERSION` = `LOBBS_VERSION` at install) marks the install and is written last. `db/` is the LoDB (`LoDb("db")` opened at the home). `apps//` and `home//` are reserved conventions. At boot `lobbsInstallInit` walks `install_mounts` in order and takes the first mount holding `lobbs/install.ls`. Later hits are logged and ignored. No marker lives on `/flash`. +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. + +LoBBS home: everything lives under `//lobbs` (`LOBBS_HOME_DIR`). `install.ls` marks the install and is written last. At boot `lobbsInstallInit` walks `install_mounts` in order and takes the first mount holding `lobbs/install.ls`. No marker lives on `/internal`. | State | Behavior | | ------- | ----------------------------------------------------------------------- | @@ -172,7 +178,11 @@ Strip 95 and 96 before `update` to get default stamps. Times are Unix seconds, n ## Verifying -Build `pio run -e rak4631` and run `trunk fmt` before commit. Native tests are not green on this branch. See `.cursor/rules/lobbs-verification.mdc`. +Product platforms are Meshtastic and MeshCore only. Build `pio run -e rak4631` (Meshtastic fork) or a MeshCore terminal-chat env, and run `trunk fmt` before commit. + +Protocol paging tests (no fork): `cd ci/host && pio test -e host -f test_protocol_host` (`LOBBS_PLATFORM_NATIVE`). + +Command integration tests use the Meshtastic fork’s `native-macos` env (`pio test -e native-macos -f test_lobbs_commands`). That runs on upstream’s Portduino desktop target. It is not a separate LoBBS platform. ## Plugin checklist diff --git a/docs/lobbs-sysop.md b/docs/lobbs-sysop.md index ea804fc..3ea440f 100644 --- a/docs/lobbs-sysop.md +++ b/docs/lobbs-sysop.md @@ -14,10 +14,11 @@ A new LoBBS node has no database until you install it. Only `/help` and `/instal | Hardware | Install mounts | | ------------------------ | ---------------------------------------------------- | -| nRF52840 | `sd`, `extra`, `flash2` (100 KB set aside for LoBBS) | -| ESP32 and other hardware | `sd`, `extra`, `flash` | +| 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` | -Mounts are listed in preference order. `sd` appears when a card is present. `extra` (about 2 MB) appears on Seeed XIAO nRF52840 kit and SenseCAP Solar Node builds (`LOBBS_EXTRA_QSPI=1`). On nRF52840 `/flash` is Meshtastic's own 28 KB and is not offered. +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. `/install ` creates the database on that mount and makes `` 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 /.` @@ -30,7 +31,7 @@ Everything LoBBS stores lives under `//lobbs`: | `//lobbs/apps//` | Reserved for per-app files | | `//lobbs/home//` | 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 `/flash`, so a Meshtastic wipe of `/flash` does not lose the install. 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 `spare`, `qspi`, or `sd`. Pulling the SD card that holds it makes the node look new until the card is back. ## Demo builds @@ -62,22 +63,22 @@ Config keys (uint32, defaults in parentheses): `session.max` (16), `session.idle | `/rm /path` | Delete a file (absolute path only) | | `/rmdir /path` | Remove an empty directory (absolute path only) | | `/rmtree /path /path` | Recursive delete (absolute path, typed twice) | -| `/mkdir path` | Create a directory under a mount (`/flash/...`, etc.) | +| `/mkdir path` | Create a directory under a mount (`/internal/...`, etc.) | | `/cp src dst` | Copy a file (no overwrite; directories refused) | | `/mv src dst` | Rename on one mount, or copy then delete across mounts (files only) | | `/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. `extra: 1.8M free of 2.0M (12% used) [lobbs]`. Free space excludes the `/flash` 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 `/flash` 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) | +| `/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 | +| `/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: `/flash/...`, `/flash2/...` on nRF52840, `/sd/...`, `/extra/...` on builds with `LOBBS_EXTRA_QSPI=1` and a QSPI flash chip. `/` lists mounts. Do not `/rmtree` `/lobbs` on your install mount unless you intend to wipe the BBS. +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` `/lobbs` on your install mount unless you intend to wipe the BBS. -`/flash` 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. `/flash2`, `/sd` and `/extra` 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. `/spare`, `/sd`, and `/qspi` have no reserve. 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. @@ -88,7 +89,7 @@ Relative paths work under your session working directory for `ls`, `cat`, `hex`, From the firmware repo root: ```bash -python src/modules/LoBBS/bin/lobbs_chunkify.py local.txt /flash/path/dest.txt +python src/modules/LoBBS/bin/lobbs_chunkify.py local.txt /internal/path/dest.txt ``` The script prints `/mkdir`, `/upload`, and `/commit` lines. Temp files use `..tmp`. diff --git a/library.json b/library.json index a26f521..320996d 100644 --- a/library.json +++ b/library.json @@ -20,10 +20,12 @@ "-", "-", "-", + "-", + "-<.pio/**>", "-<.cursor/**>", "-", "-" ], - "extraScript": "platforms/meshtastic/extra_script.py" + "extraScript": "platforms/extra_script.py" } } diff --git a/lodb/LoDB.cpp b/lodb/LoDB.cpp index 1e642ab..4ea0350 100644 --- a/lodb/LoDB.cpp +++ b/lodb/LoDB.cpp @@ -1,42 +1,35 @@ -#include #include -#if __has_include("configuration.h") -#include "configuration.h" -#include "gps/RTC.h" +#include "platforms/LoPlatform.h" + #undef LODB_LOG_DEBUG -#define LODB_LOG_DEBUG(...) LOG_DEBUG(__VA_ARGS__) +#define LODB_LOG_DEBUG(...) LOBBS_LOG_DEBUG(__VA_ARGS__) #undef LODB_LOG_INFO -#define LODB_LOG_INFO(...) LOG_INFO(__VA_ARGS__) +#define LODB_LOG_INFO(...) LOBBS_LOG_INFO(__VA_ARGS__) #undef LODB_LOG_WARN -#define LODB_LOG_WARN(...) LOG_WARN(__VA_ARGS__) +#define LODB_LOG_WARN(...) LOBBS_LOG_WARN(__VA_ARGS__) #undef LODB_LOG_ERROR -#define LODB_LOG_ERROR(...) LOG_ERROR(__VA_ARGS__) -#endif +#define LODB_LOG_ERROR(...) LOBBS_LOG_ERROR(__VA_ARGS__) #include #include #include #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static_assert(LODB_FILE_IO_BUFFER_SIZE <= LODB_MAX_RECORD_FILE_BYTES, "encode buffer cannot exceed max on-disk record (get would reject writes)"); static constexpr size_t LODB_RECORD_PATH_BYTES = 192; -__attribute__((weak)) uint32_t lodb_now_ms(void) +uint32_t lodb_now_ms(void) { - return static_cast(millis()); + return lobbsPlatformMillis(); } -__attribute__((weak)) uint32_t lodb_now_unix(void) +uint32_t lodb_now_unix(void) { -#if __has_include("configuration.h") - return getTime(); -#else - return 0; -#endif + return lobbsPlatformUnixNow(); } static void lodbStampInsert(lodb_uuid_t uuid, LoScalar &record) @@ -80,7 +73,7 @@ lodb_uuid_t lodb_new_uuid(const char *str, uint64_t salt) if (str == nullptr) { uint32_t timestamp = lodb_now_ms(); - uint32_t random_val = (uint32_t)random(0x7fffffff) ^ ((uint32_t)random(0x7fffffff) << 1); + uint32_t random_val = lobbsPlatformRandom(0x7fffffff) ^ (lobbsPlatformRandom(0x7fffffff) << 1); snprintf(generated_str, sizeof(generated_str), "%u:%u", timestamp, random_val); input_str = generated_str; } @@ -159,7 +152,7 @@ LoDb::~LoDb() {} /** Calls fn for each `<16 hex>.ls` record in `dir_path`. False when the path is not a directory. */ static bool lodbForEachRecord(const char *dir_path, const std::function &fn) { - File dir = LoFS::open(dir_path, FILE_O_READ); + LoFile dir = LoFS::open(dir_path, "r"); if (!dir) { LODB_LOG_DEBUG("Table directory not found: %s", dir_path); return true; @@ -170,7 +163,7 @@ static bool lodbForEachRecord(const char *dir_path, const std::function -#include -#include +#include "LoFS.h" +#include "volumes/LoFSVolume.h" +#include +#include #include -#if LOBBS_EXTRA_QSPI -#if !defined(NRF52840_XXAA) || !defined(PIN_QSPI_SCK) -#error LOBBS_EXTRA_QSPI requires NRF52840_XXAA and PIN_QSPI_SCK -#endif -#include "LoFSQspi.h" -#endif +#include "core/LoBBSStackGuard.h" -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) -#include -#include - -#ifdef SDCARD_USE_SPI1 -extern SPIClass SPI_HSPI; -#define SDHandler SPI_HSPI -#else -#define SDHandler SPI -#endif - -#ifndef SD_SPI_FREQUENCY -#define SD_SPI_FREQUENCY 4000000U -#endif -#endif - -#if defined(ARCH_NRF52) -#include "flash/flash_nrf5x.h" - -/** Second internal-flash partition bounds from the nRF52840 linker scripts. Weak: other nRF linker scripts get no `/flash2`. */ -extern "C" uint8_t __flash2_start[] __attribute__((weak)); -extern "C" uint8_t __flash2_end[] __attribute__((weak)); - -static constexpr uint32_t LOFS_FLASH2_BLOCK = 128; - -// The flash_nrf5x page cache is shared with InternalFS (which the BLE task also writes), so /flash2 block IO holds its lock. -static int lofsFlash2Read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) -{ - InternalFS._lockFS(); - int n = flash_nrf5x_read(buffer, (uint32_t)c->context + block * LOFS_FLASH2_BLOCK + off, size); - InternalFS._unlockFS(); - return n > 0 ? 0 : -1; -} - -static int lofsFlash2Prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) -{ - InternalFS._lockFS(); - int n = flash_nrf5x_write((uint32_t)c->context + block * LOFS_FLASH2_BLOCK + off, buffer, size); - InternalFS._unlockFS(); - return n > 0 ? 0 : -1; -} - -static int lofsFlash2Erase(const struct lfs_config *c, lfs_block_t block) -{ - uint8_t ff[LOFS_FLASH2_BLOCK]; - memset(ff, 0xFF, sizeof(ff)); - InternalFS._lockFS(); - int n = flash_nrf5x_write((uint32_t)c->context + block * LOFS_FLASH2_BLOCK, ff, sizeof(ff)); - InternalFS._unlockFS(); - return n > 0 ? 0 : -1; -} - -static int lofsFlash2Sync(const struct lfs_config *c) -{ - (void)c; - InternalFS._lockFS(); - flash_nrf5x_flush(); - InternalFS._unlockFS(); - return 0; -} - -static struct lfs_config lofsFlash2Cfg; -static Adafruit_LittleFS lofsFlash2(&lofsFlash2Cfg); - -static bool lofsFlash2Format() -{ - const uint32_t start = (uint32_t)lofsFlash2Cfg.context; - const uint32_t end = start + lofsFlash2Cfg.block_count * LOFS_FLASH2_BLOCK; - LOG_WARN("LoFS: formatting /flash2 (0x%x-0x%x)", (unsigned)start, (unsigned)end); - LOBBS_BOOT_STEP("flash2: erase partition pages"); - InternalFS._lockFS(); - flash_nrf5x_flush(); - for (uint32_t addr = start; addr < end; addr += FLASH_NRF52_PAGE_SIZE) - flash_nrf5x_erase(addr); - InternalFS._unlockFS(); - LOBBS_BOOT_STEP("flash2: lfs format"); - return lofsFlash2.format() && lofsFlash2.begin(); -} - -static bool lofsFlash2Begin() -{ - LOBBS_BOOT_STEP("flash2: begin enter"); - const uint32_t start = (uint32_t)__flash2_start; - const uint32_t end = (uint32_t)__flash2_end; - if (!start || end <= start) - return false; - lofsFlash2Cfg.context = (void *)start; - lofsFlash2Cfg.read = lofsFlash2Read; - lofsFlash2Cfg.prog = lofsFlash2Prog; - lofsFlash2Cfg.erase = lofsFlash2Erase; - lofsFlash2Cfg.sync = lofsFlash2Sync; - lofsFlash2Cfg.read_size = LOFS_FLASH2_BLOCK; - lofsFlash2Cfg.prog_size = LOFS_FLASH2_BLOCK; - lofsFlash2Cfg.block_size = LOFS_FLASH2_BLOCK; - lofsFlash2Cfg.block_count = (end - start) / LOFS_FLASH2_BLOCK; - lofsFlash2Cfg.lookahead = 128; - LOBBS_BOOT_STEP("flash2: lfs mount"); - if (lofsFlash2.begin()) - return true; - return lofsFlash2Format(); -} - -#if LOBBS_EXTRA_QSPI -static struct lfs_config lofsQspiCfg; -static Adafruit_LittleFS lofsQspi(&lofsQspiCfg); - -static bool lofsQspiFormat() -{ - LOG_WARN("LoFS: formatting /extra (QSPI)"); - LOBBS_BOOT_STEP("extra: lfs format"); - return lofsQspi.format() && lofsQspi.begin(); -} - -static bool lofsQspiBegin() -{ - LOBBS_BOOT_STEP("extra: begin enter"); - LOBBS_BOOT_STEP("extra: qspi config"); - if (!lofsQspiConfig(lofsQspiCfg)) - return false; - LOBBS_BOOT_STEP("extra: lfs mount"); - if (lofsQspi.begin()) - return true; - return lofsQspiFormat(); -} -#endif - -static Adafruit_LittleFS &lofsFs(LoFS::Backend b) -{ - if (b == LoFS::Backend::Flash2) - return lofsFlash2; -#if LOBBS_EXTRA_QSPI - if (b == LoFS::Backend::Extra) - return lofsQspi; -#endif - return FSCom; -} -#else -static auto &lofsFs(bool flash2) -{ - (void)flash2; - return FSCom; -} -#endif - -#include "../apps/AppUtil.h" -#include "LoBBSStackGuard.h" -#include - -LoFS::Mount LoFS::mounts[4]; +LoFS::Mount LoFS::mounts[6]; int LoFS::mountCount = 0; bool LoFS::begun = false; -static bool lobfsSdPresent() +static uint32_t lofsCrc32Update(uint32_t crc, const uint8_t *data, size_t len) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - uint8_t cardType = SD.cardType(); - if (cardType == CARD_NONE) { - concurrency::LockGuard g(spiLock); - SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); - if (SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) - cardType = SD.cardType(); + for (size_t i = 0; data && i < len; i++) { + crc ^= data[i]; + for (int b = 0; b < 8; b++) + crc = (crc >> 1) ^ (0xedb88320 & (~((crc & 1) - 1))); } - return cardType != CARD_NONE; -#else - return false; -#endif + return crc; } -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) -static const char *convertToSDMode(const char *modeStr) +static int lofsMountPreferenceRank(const char *name) { - return modeStr; + if (!name) + return 999; + for (int i = 0; i < LOFS_MOUNT_ORDER_LEN; i++) { + if (strcmp(name, LOFS_MOUNT_ORDER[i]) == 0) + return i; + } + return 999; } -static const char *convertToSDMode(uint8_t mode) + +int LoFS::mountPreferenceRank(const char *name) { - return (mode == 0) ? "r" : "w"; + return lofsMountPreferenceRank(name); } -#else -static uint8_t convertToSDMode(const char *modeStr) -{ - return (strcmp(modeStr, "r") == 0) ? FILE_READ : FILE_WRITE; -} -static uint8_t convertToSDMode(uint8_t mode) -{ - return (mode == 0) ? FILE_READ : FILE_WRITE; -} -#endif -#endif void LoFS::begin() { if (begun) return; mountCount = 0; - bool hasFlash2 = false; - // Registration order is install preference order. -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - LOBBS_BOOT_STEP("LoFS: sd present check"); - if (lobfsSdPresent()) - mounts[mountCount++] = Mount{"sd", Backend::Sd, true, false, true}; -#endif + LoFSMountTable table; + lofsPlatformMounts(table); -#if LOBBS_EXTRA_QSPI - LOBBS_BOOT_STEP("LoFS: lofsQspiBegin"); - if (lofsQspiBegin()) - mounts[mountCount++] = Mount{"extra", Backend::Extra, true, false, true}; -#endif - -#if defined(ARCH_NRF52) - LOBBS_BOOT_STEP("LoFS: lofsFlash2Begin"); - if (lofsFlash2Begin()) { - mounts[mountCount++] = Mount{"flash2", Backend::Flash2, true, false, true}; - hasFlash2 = true; + for (int o = 0; o < LOFS_MOUNT_ORDER_LEN && mountCount < LOFS_MAX_MOUNTS; o++) { + const char *want = LOFS_MOUNT_ORDER[o]; + for (int i = 0; i < table.count; i++) { + if (strcmp(table.specs[i].name, want) != 0) + continue; + LoFSMountSpec &spec = table.specs[i]; + if (!spec.volume || !spec.volume->begin()) + break; + mounts[mountCount++] = {spec.name, spec.volume, true, spec.shared, false, spec.formattable}; + break; + } + } + + for (int i = 0; i < mountCount; i++) { + if (!mounts[i].shared) { + mounts[i].dbSafe = true; + continue; + } + mounts[i].dbSafe = true; + const int selfRank = lofsMountPreferenceRank(mounts[i].name); + for (int j = 0; j < mountCount; j++) { + if (j == i || !mounts[j].present) + continue; + if (lofsMountPreferenceRank(mounts[j].name) < selfRank) { + mounts[i].dbSafe = false; + break; + } + } } -#endif - LOBBS_BOOT_STEP("LoFS: register flash mount"); - mounts[mountCount++] = Mount{"flash", Backend::Flash, true, true, !hasFlash2}; begun = true; - LOBBS_BOOT_STEP("LoFS: begin done"); } +#ifdef PIO_UNIT_TESTING +void LoFS::resetForTests() +{ + begun = false; + mountCount = 0; +} +#endif + LoFS::Mount *LoFS::findByName(const char *name, size_t len) { for (int i = 0; i < mountCount; i++) { @@ -260,23 +109,9 @@ bool LoFS::mountDbSafe(const char *name) bool LoFS::format(const char *name) { Mount *m = name ? findByName(name, strlen(name)) : nullptr; - if (!m) + if (!m || !m->formattable || !m->volume) return false; -#if defined(ARCH_NRF52) - if (m->backend == Backend::Flash2) - return lofsFlash2Format(); -#endif -#if LOBBS_EXTRA_QSPI - if (m->backend == Backend::Extra) - return lofsQspiFormat(); -#endif -#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) - if (m->backend == Backend::Flash) { - concurrency::LockGuard g(spiLock); - return FSCom.format(); - } -#endif - return false; + return m->volume->format(); } void LoFS::eachPresentMount(void (*fn)(void *ctx, const char *name), void *ctx) @@ -310,7 +145,7 @@ bool LoFS::resolve(const char *filepath, Resolved &out) if (!m) return false; - out.backend = m->backend; + out.volume = m->volume; if (!slash || slash[1] == '\0') { out.kind = PathKind::MountRoot; out.rel = "/"; @@ -324,7 +159,7 @@ bool LoFS::resolve(const char *filepath, Resolved &out) const char *LoFS::backendPath(const Resolved &r) { - return r.backend == Backend::Sd ? r.rel + 1 : r.rel; + return r.rel; } bool LoFS::isMountPoint(const char *path) @@ -359,32 +194,7 @@ bool LoFS::isDirectory(const char *path) return false; if (r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot) return true; - - const char *bp = backendPath(r); - - concurrency::LockGuard g(spiLock); -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - File f; -#else - File f(FSCom); -#endif -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - f = SD.open(bp, FILE_O_READ); -#endif - } else { - f = lofsFs(r.backend == Backend::Flash2).open(bp, FILE_O_READ); - } -#else - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - f = SD.open(bp, FILE_O_READ); -#endif - } else { - f = lofsFs(r.backend).open(bp, FILE_O_READ); - } -#endif + LoFile f = open(path, "r"); if (!f) return false; bool isDir = f.isDirectory(); @@ -397,58 +207,20 @@ bool LoFS::refuseMountPointMutation(const char *filepath) return isMountPoint(filepath); } -File LoFS::open(const char *filepath, uint8_t mode) +LoFile LoFS::open(const char *filepath, uint8_t mode) { -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - const char *modeStr = (mode == 0) ? FILE_O_READ : FILE_O_WRITE; - return open(filepath, modeStr); -#else Resolved r; - if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot) { - return File(FSCom); - } - - const char *bp = backendPath(r); - - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.open(bp, convertToSDMode(mode)); -#endif - } - return lofsFs(r.backend).open(bp, mode); -#endif + if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || !r.volume) + return LoFile(); + return r.volume->open(backendPath(r), mode); } -File LoFS::open(const char *filepath, const char *mode) +LoFile LoFS::open(const char *filepath, const char *mode) { Resolved r; - if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot) { -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - return File(); -#else - return File(FSCom); -#endif - } - - const char *bp = backendPath(r); - - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - return SD.open(bp, mode); -#else - return SD.open(bp, convertToSDMode(mode)); -#endif -#endif - } -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - return lofsFs(r.backend == Backend::Flash2).open(bp, mode); -#else - uint8_t flashMode = (mode && strcmp(mode, "r") == 0) ? 0 : 1; - return lofsFs(r.backend).open(bp, flashMode); -#endif + if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || !r.volume) + return LoFile(); + return r.volume->open(backendPath(r), mode); } bool LoFS::exists(const char *filepath) @@ -460,103 +232,48 @@ bool LoFS::exists(const char *filepath) return true; if (r.kind == PathKind::MountRoot) return true; - - const char *bp = backendPath(r); - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.exists(bp); -#endif - } -#if defined(ARCH_NRF52) - return lofsFs(r.backend).exists(bp); -#else - return lofsFs(r.backend == Backend::Flash2).exists(bp); -#endif + return r.volume && r.volume->exists(backendPath(r)); } bool LoFS::mkdir(const char *filepath) { if (refuseMountPointMutation(filepath)) return false; - Resolved r; - if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot) + if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || !r.volume) return false; - - const char *bp = backendPath(r); - - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.mkdir(bp); -#endif - } -#if defined(ARCH_NRF52) - return lofsFs(r.backend).mkdir(bp); -#else - return lofsFs(r.backend == Backend::Flash2).mkdir(bp); -#endif + return r.volume->mkdir(backendPath(r)); } bool LoFS::remove(const char *filepath) { if (refuseMountPointMutation(filepath)) return false; - Resolved r; - if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot) + if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot || !r.volume) return false; - - const char *bp = backendPath(r); - - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.remove(bp); -#endif - } -#if defined(ARCH_NRF52) - return lofsFs(r.backend).remove(bp); -#else - return lofsFs(r.backend == Backend::Flash2).remove(bp); -#endif + return r.volume->remove(backendPath(r)); } bool LoFS::rename(const char *oldfilepath, const char *newfilepath) { if (refuseMountPointMutation(oldfilepath) || refuseMountPointMutation(newfilepath)) return false; - Resolved oldR; Resolved newR; - if (!resolve(oldfilepath, oldR) || !resolve(newfilepath, newR)) + if (!resolve(oldfilepath, oldR) || !resolve(newfilepath, newR) || !oldR.volume || !newR.volume) return false; if (oldR.kind == PathKind::VirtualRoot || newR.kind == PathKind::VirtualRoot) return false; - if (oldR.backend != newR.backend) + if (oldR.volume != newR.volume) return false; - - const char *oldBp = backendPath(oldR); - const char *newBp = backendPath(newR); - - concurrency::LockGuard g(spiLock); - if (oldR.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.rename(oldBp, newBp); -#endif - } -#if defined(ARCH_NRF52) - return lofsFs(oldR.backend).rename(oldBp, newBp); -#else - return lofsFs(oldR.backend == Backend::Flash2).rename(oldBp, newBp); -#endif + return oldR.volume->rename(backendPath(oldR), backendPath(newR)); } -static bool lobfsEachDirEntry(File &dir, void *ctx, LoFS::ListCallback fn) +static bool lobfsEachDirEntry(LoFile &dir, void *ctx, LoFS::ListCallback fn) { while (true) { - File file = dir.openNextFile(); + LoFile file = dir.openNextFile(); if (!file) break; @@ -596,7 +313,7 @@ bool LoFS::list(const char *dirpath, void *ctx, ListCallback fn) return true; } - File dir = open(dirpath, FILE_O_READ); + LoFile dir = open(dirpath, "r"); if (!dir) return false; if (!dir.isDirectory()) { @@ -625,7 +342,7 @@ bool LoFS::stat(const char *filepath, uint32_t *sizeOut, bool *isDirOut) return true; } - File f = open(filepath, FILE_O_READ); + LoFile f = open(filepath, "r"); if (!f) return false; if (isDirOut) @@ -648,12 +365,12 @@ bool LoFS::writeAt(const char *filepath, uint32_t offset, const uint8_t *data, s return false; const bool creating = !exists(filepath); -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) - File f = open(filepath, creating ? "w" : "r+"); -#else - (void)creating; - File f = open(filepath, (uint8_t)1); -#endif + bool appendAtEnd = false; + LoFile f = open(filepath, creating ? "w" : "r+"); + if (!f && !creating && offset > 0) { + f = open(filepath, "a"); + appendAtEnd = true; + } if (!f) return false; if (f.isDirectory()) { @@ -662,15 +379,14 @@ bool LoFS::writeAt(const char *filepath, uint32_t offset, const uint8_t *data, s } bool ok = false; - { - concurrency::LockGuard g(spiLock); - if (f.seek(offset)) { - size_t w = f.write(data, len); - ok = (w == len); - } - f.flush(); + if (!appendAtEnd && offset > 0 && !f.seek(offset)) { f.close(); + return false; } + size_t w = f.write(data, len); + ok = (w == len); + f.flush(); + f.close(); return ok; } @@ -690,7 +406,7 @@ bool LoFS::copy(const char *src, const char *dst) if (dstR.kind == PathKind::VirtualRoot || dstR.kind == PathKind::MountRoot) return false; - File srcFile = open(src, FILE_O_READ); + LoFile srcFile = open(src, "r"); if (!srcFile) return false; if (srcFile.isDirectory()) { @@ -698,7 +414,7 @@ bool LoFS::copy(const char *src, const char *dst) return false; } - File dstFile = open(dst, FILE_O_WRITE); + LoFile dstFile = open(dst, "w"); if (!dstFile) { srcFile.close(); return false; @@ -707,30 +423,19 @@ bool LoFS::copy(const char *src, const char *dst) unsigned char buffer[128]; bool ok = true; while (true) { - size_t n = 0; - { - concurrency::LockGuard g(spiLock); - n = srcFile.read(buffer, sizeof(buffer)); - } + size_t n = srcFile.read(buffer, sizeof(buffer)); if (n == 0) break; - size_t w = 0; - { - concurrency::LockGuard g(spiLock); - w = dstFile.write(buffer, n); - } + size_t w = dstFile.write(buffer, n); if (w != n) { ok = false; break; } } - { - concurrency::LockGuard g(spiLock); - dstFile.flush(); - dstFile.close(); - srcFile.close(); - } + dstFile.flush(); + dstFile.close(); + srcFile.close(); if (!ok) remove(dst); @@ -773,7 +478,7 @@ bool LoFS::crc32File(const char *filepath, uint32_t *crcOut) { if (!filepath || !crcOut) return false; - File f = open(filepath, FILE_O_READ); + LoFile f = open(filepath, "r"); if (!f) return false; if (f.isDirectory()) { @@ -786,7 +491,7 @@ bool LoFS::crc32File(const char *filepath, uint32_t *crcOut) size_t n = f.read(chunk, sizeof(chunk)); if (n == 0) break; - crc = lobbsCrc32Update(crc, chunk, n); + crc = lofsCrc32Update(crc, chunk, n); } f.close(); *crcOut = ~crc; @@ -806,59 +511,17 @@ LoFSMoveResult LoFS::moveIfCrc32Matches(const char *src, const char *dst, uint32 uint64_t LoFS::totalBytes(const char *mountRoot) { Resolved r; - if (!resolve(mountRoot, r) || r.kind != PathKind::MountRoot) + if (!resolve(mountRoot, r) || r.kind != PathKind::MountRoot || !r.volume) return 0; - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.totalBytes(); -#endif - } -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) - return FSCom.totalBytes(); -#elif defined(ARCH_NRF52) - const lfs_config *cfg = lofsFs(r.backend)._getFS()->cfg; - return cfg ? (uint64_t)cfg->block_size * cfg->block_count : 0; -#else - return 0; -#endif + return r.volume->totalBytes(); } -#if defined(ARCH_NRF52) -static int lofsCountBlock(void *ctx, lfs_block_t block) -{ - (void)block; - (*(uint32_t *)ctx)++; - return 0; -} -#endif - uint64_t LoFS::usedBytes(const char *mountRoot) { Resolved r; - if (!resolve(mountRoot, r) || r.kind != PathKind::MountRoot) + if (!resolve(mountRoot, r) || r.kind != PathKind::MountRoot || !r.volume) return 0; - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.usedBytes(); -#endif - } -#if defined(ARCH_ESP32) || defined(ARCH_RP2040) - return FSCom.usedBytes(); -#elif defined(ARCH_NRF52) - Adafruit_LittleFS &fs = lofsFs(r.backend); - lfs_t *lfs = fs._getFS(); - uint32_t blocks = 0; - fs._lockFS(); - int err = lfs_traverse(lfs, lofsCountBlock, &blocks); - fs._unlockFS(); - if (err || !lfs->cfg) - return 0; - return (uint64_t)blocks * lfs->cfg->block_size; -#else - return 0; -#endif + return r.volume->usedBytes(); } uint64_t LoFS::freeBytes(const char *mountRoot) @@ -873,14 +536,14 @@ uint64_t LoFS::freeBytes(const char *mountRoot) uint32_t LoFS::mountReserve(const char *name) { Mount *m = name ? findByName(name, strlen(name)) : nullptr; - return (m && m->shared) ? LOFS_SHARED_RESERVE_BYTES : 0; + return (m && m->shared) ? lofsPlatformSharedReserve(name) : 0; } bool LoFS::hasRoom(const char *path, uint32_t bytes) { Resolved r; const char *name = mountNameForPath(path); - if (!name || !resolve(path, r)) + if (!name || !resolve(path, r) || !r.volume) return true; char root[20]; snprintf(root, sizeof(root), "/%s", name); @@ -891,17 +554,8 @@ bool LoFS::hasRoom(const char *path, uint32_t bytes) const uint64_t freeB = used < total ? total - used : 0; uint32_t block = 4096; - uint32_t slack = 2 * 4096; - if (r.backend == Backend::Sd) { - block = 512; - slack = 64 * 1024; - } -#if defined(ARCH_NRF52) - if (r.backend == Backend::Flash || r.backend == Backend::Flash2) { - block = 128; - slack = 4 * 128; - } -#endif + uint32_t slack = 8192; + r.volume->spaceHint(&block, &slack); const uint64_t need = (((uint64_t)bytes + block - 1) / block) * block + slack + mountReserve(name); return freeB >= need; } @@ -915,7 +569,7 @@ bool LoFS::rmdir(const char *filepath, bool recursive) return true; if (recursive) { - File dir = open(filepath, FILE_O_READ); + LoFile dir = open(filepath, "r"); if (!dir) return false; @@ -926,7 +580,7 @@ bool LoFS::rmdir(const char *filepath, bool recursive) bool result = true; while (true) { - File file = dir.openNextFile(); + LoFile file = dir.openNextFile(); if (!file) break; @@ -957,20 +611,8 @@ bool LoFS::rmdir(const char *filepath, bool recursive) } Resolved r; - if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot) + if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot || !r.volume) return false; - const char *bp = backendPath(r); - - concurrency::LockGuard g(spiLock); - if (r.backend == Backend::Sd) { -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - return SD.rmdir(bp); -#endif - } -#if defined(ARCH_NRF52) - return lofsFs(r.backend).rmdir(bp); -#else - return lofsFs(r.backend == Backend::Flash2).rmdir(bp); -#endif + return r.volume->rmdir(backendPath(r)); } diff --git a/lofs/LoFS.h b/lofs/LoFS.h index 80d8731..de6838c 100644 --- a/lofs/LoFS.h +++ b/lofs/LoFS.h @@ -1,38 +1,12 @@ #pragma once -#define LOFS_VERSION "0.3.0" +#define LOFS_VERSION "0.5.0" -#include "FSCommon.h" -#include "configuration.h" -#include - -#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) -#include -#include -#ifndef FILE_READ -#define FILE_READ O_READ -#endif -#ifndef FILE_WRITE -#define FILE_WRITE O_WRITE -#endif -#endif - -/** Free space LoFS keeps on shared mounts (`/flash`) so Meshtastic can still rewrite its own files. */ -#ifndef LOFS_SHARED_RESERVE_BYTES -#if defined(ARCH_NRF52) -#define LOFS_SHARED_RESERVE_BYTES (16 * 1024) -#elif defined(ARCH_PORTDUINO) -#define LOFS_SHARED_RESERVE_BYTES 0 -#else -#define LOFS_SHARED_RESERVE_BYTES (128 * 1024) -#endif -#endif +#include "LoFile.h" /** - * Unified filesystem with a mount table in install preference order: /sd (when present), /extra (when - * LOBBS_EXTRA_QSPI), /flash2 (nRF52840 second internal-flash partition), /flash. + * Unified filesystem with a mount table in install preference order: /sd, /qspi, /reserve, /spare, /internal. * "/" is a virtual root that lists mounts. Every other path must start with "//..." or "/". - * Absolute paths only; no glob metacharacters. Upload gap policy lives in FsCommands, not writeAt. */ enum class LoFSMoveResult : uint8_t { Ok, @@ -48,15 +22,19 @@ enum class LoFSMoveResult : uint8_t { Failed, }; +class LoFSVolume; + class LoFS { public: - enum class Backend : uint8_t { Flash = 0, Sd = 1, Extra = 2, Flash2 = 3 }; - static void begin(); +#ifdef PIO_UNIT_TESTING + /** Clears mount state so begin() can run again (host / Unity harness). */ + static void resetForTests(); +#endif - static File open(const char *filepath, uint8_t mode); - static File open(const char *filepath, const char *mode); + static LoFile open(const char *filepath, uint8_t mode); + static LoFile open(const char *filepath, const char *mode); static bool exists(const char *filepath); static bool mkdir(const char *filepath); @@ -65,46 +43,38 @@ class LoFS static bool rmdir(const char *filepath, bool recursive = false); static bool copy(const char *src, const char *dst); - /** Same-mount rename or cross-mount file copy+delete. */ static LoFSMoveResult move(const char *src, const char *dst); static bool crc32File(const char *filepath, uint32_t *crcOut); static LoFSMoveResult moveIfCrc32Matches(const char *src, const char *dst, uint32_t expectedCrc); - /** Write len bytes at offset; extends file. Does not truncate an existing file. */ static bool writeAt(const char *filepath, uint32_t offset, const uint8_t *data, size_t len); static bool stat(const char *filepath, uint32_t *sizeOut, bool *isDirOut); static uint64_t totalBytes(const char *mountRoot); static uint64_t usedBytes(const char *mountRoot); static uint64_t freeBytes(const char *mountRoot); - /** Bytes LoFS writes leave free on this mount (`LOFS_SHARED_RESERVE_BYTES` on shared mounts, else 0). */ static uint32_t mountReserve(const char *name); - /** True when writing `bytes` under `path` leaves the mount's reserve free. True when the mount reports no size or - * the path does not resolve (the write itself then fails). */ static bool hasRoom(const char *path, uint32_t bytes); - /** True when a database may live on this mount. `flash` only qualifies when there is no `flash2` mount. */ static bool mountDbSafe(const char *name); - /** Erase and reformat a dedicated mount (`flash2`, `extra`). False for shared or SD mounts. */ static bool format(const char *name); static bool isMountPoint(const char *path); static bool isDirectory(const char *path); - /** Returns mount name ("sd", "extra", "flash2", "flash") or nullptr. */ static const char *mountNameForPath(const char *path); static bool mountPresent(const char *name); static void eachPresentMount(void (*fn)(void *ctx, const char *name), void *ctx); - /** `size` is the file length in bytes; 0 for directories and mounts. */ typedef bool (*ListCallback)(void *ctx, const char *basename, bool isDirectory, uint32_t size); static bool list(const char *dirpath, void *ctx, ListCallback fn); private: struct Mount { const char *name; - Backend backend; + LoFSVolume *volume; bool present; bool shared; bool dbSafe; + bool formattable; }; static Mount mounts[]; @@ -115,8 +85,7 @@ class LoFS struct Resolved { PathKind kind = PathKind::Invalid; - Backend backend = Backend::Flash; - /** Points into the caller's path (or a static "/"); valid while that path is. */ + LoFSVolume *volume = nullptr; const char *rel = nullptr; }; @@ -124,4 +93,5 @@ class LoFS static Mount *findByName(const char *name, size_t len); static bool refuseMountPointMutation(const char *filepath); static const char *backendPath(const Resolved &r); + static int mountPreferenceRank(const char *name); }; diff --git a/lofs/LoFSQspi.h b/lofs/LoFSQspi.h deleted file mode 100644 index ff5dd93..0000000 --- a/lofs/LoFSQspi.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "LoBBSConfig.h" - -#if LOBBS_EXTRA_QSPI - -struct lfs_config; - -/** Init nrfx QSPI (once), verify JEDEC, fill LittleFS block callbacks. Returns false if hardware missing. */ -bool lofsQspiConfig(struct lfs_config &cfg); - -#endif diff --git a/lofs/LoFile.cpp b/lofs/LoFile.cpp new file mode 100644 index 0000000..be4ef56 --- /dev/null +++ b/lofs/LoFile.cpp @@ -0,0 +1,98 @@ +#include "LoFile.h" + +LoFile LoFileAdopt(const LoFileOps *ops, void *handle) +{ + LoFile out; + if (!ops || !handle) + return out; + out.storage_ = new LoFile::Storage(); + out.storage_->ops = ops; + out.storage_->handle = handle; + return out; +} + +LoFile::LoFile() : storage_(nullptr) {} + +LoFile::~LoFile() +{ + close(); +} + +LoFile::LoFile(LoFile &&other) noexcept : storage_(other.storage_) +{ + other.storage_ = nullptr; +} + +LoFile &LoFile::operator=(LoFile &&other) noexcept +{ + if (this != &other) { + close(); + storage_ = other.storage_; + other.storage_ = nullptr; + } + return *this; +} + +LoFile::operator bool() const +{ + return storage_ && storage_->ops && storage_->handle && storage_->ops->valid(storage_->handle); +} + +void LoFile::close() +{ + if (storage_) { + if (storage_->ops && storage_->handle) + storage_->ops->close(storage_->handle); + delete storage_; + storage_ = nullptr; + } +} + +size_t LoFile::read(uint8_t *buf, size_t len) +{ + return (storage_ && storage_->ops && storage_->handle) ? storage_->ops->read(storage_->handle, buf, len) : 0; +} + +size_t LoFile::write(const uint8_t *buf, size_t len) +{ + return (storage_ && storage_->ops && storage_->handle) ? storage_->ops->write(storage_->handle, buf, len) : 0; +} + +bool LoFile::seek(uint32_t pos) +{ + return storage_ && storage_->ops && storage_->handle && storage_->ops->seek(storage_->handle, pos); +} + +uint32_t LoFile::size() const +{ + return (storage_ && storage_->ops && storage_->handle) ? storage_->ops->size(storage_->handle) : 0; +} + +bool LoFile::isDirectory() const +{ + return storage_ && storage_->ops && storage_->handle && storage_->ops->isDirectory(storage_->handle); +} + +const char *LoFile::name() const +{ + return (storage_ && storage_->ops && storage_->handle) ? storage_->ops->name(storage_->handle) : ""; +} + +void LoFile::flush() +{ + if (storage_ && storage_->ops && storage_->handle) + storage_->ops->flush(storage_->handle); +} + +LoFile LoFile::openNextFile() +{ + LoFile out; + if (!storage_ || !storage_->ops || !storage_->handle) + return out; + void *next = storage_->ops->openNextFile(storage_->handle); + if (!next) + return out; + return LoFileAdopt(storage_->ops, next); +} + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/LoFile.h b/lofs/LoFile.h new file mode 100644 index 0000000..ac0d340 --- /dev/null +++ b/lofs/LoFile.h @@ -0,0 +1,37 @@ +#pragma once +#include "LoFileOps.h" +#include +#include + +/** Platform-neutral open file handle. */ +class LoFile { + public: + LoFile(); + ~LoFile(); + LoFile(LoFile &&other) noexcept; + LoFile &operator=(LoFile &&other) noexcept; + LoFile(const LoFile &) = delete; + LoFile &operator=(const LoFile &) = delete; + + explicit operator bool() const; + + size_t read(uint8_t *buf, size_t len); + size_t write(const uint8_t *buf, size_t len); + bool seek(uint32_t pos); + uint32_t size() const; + bool isDirectory() const; + const char *name() const; + void close(); + void flush(); + LoFile openNextFile(); + + private: + friend LoFile LoFileAdopt(const LoFileOps *ops, void *handle); + struct Storage { + const LoFileOps *ops = nullptr; + void *handle = nullptr; + }; + Storage *storage_; +}; + +LoFile LoFileAdopt(const LoFileOps *ops, void *handle); diff --git a/lofs/LoFileOps.h b/lofs/LoFileOps.h new file mode 100644 index 0000000..e49abf1 --- /dev/null +++ b/lofs/LoFileOps.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +/** Type-erased Arduino-style file handle for LoFile. */ +struct LoFileOps { + void (*destroy)(void *handle); + bool (*valid)(void *handle); + size_t (*read)(void *handle, uint8_t *buf, size_t len); + size_t (*write)(void *handle, const uint8_t *buf, size_t len); + bool (*seek)(void *handle, uint32_t pos); + uint32_t (*size)(void *handle); + bool (*isDirectory)(void *handle); + const char *(*name)(void *handle); + void (*close)(void *handle); + void (*flush)(void *handle); + /** Returns a new heap handle for the next directory entry, or nullptr. */ + void *(*openNextFile)(void *dirHandle); +}; diff --git a/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp b/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp new file mode 100644 index 0000000..889580c --- /dev/null +++ b/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp @@ -0,0 +1,109 @@ +#if defined(LOBBS_PLATFORM_MESHCORE) +#include "core/LoBBSArch.h" +#include "LoFSMountsMeshcore.h" +#include "lofs/volumes/LoFSVolume.h" +#include "lofs/volumes/LoFSLittleFsVolume.h" +#include "lofs/volumes/LoFSNrfFlashBlockDevice.h" +#include "lofs/volumes/LoFSQspiNorBlockDevice.h" +#include "core/LoBBSConfig.h" +#if LOBBS_ARCH_ESP32 +#include "lofs/volumes/LoFSArduinoFsVolume.h" +#include +#endif + +#if LOBBS_ARCH_RP2040 +#include "lofs/volumes/LoFSArduinoFsVolume.h" +#include +#endif + +#if defined(LOFS_NRF52) +#include +extern "C" uint8_t __flash3_start[] __attribute__((weak)); +extern "C" uint8_t __flash3_end[] __attribute__((weak)); +#if defined(EXTRAFS) && !defined(QSPIFLASH) +#include +extern CustomLFS ExtraFS; +#endif +#if defined(QSPIFLASH) +#include +extern CustomLFS_QSPIFlash QSPIFlash; +#endif +#endif + +#if LOBBS_ARCH_ESP32 +#include +#endif + +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); +#endif +#endif + +#if LOBBS_ARCH_ESP32 +LoFSArduinoFsVolume gInternal; +#endif + +#if LOBBS_ARCH_RP2040 +LoFSArduinoFsVolume gInternal; +#endif + +} // namespace + +#if defined(LOFS_NRF52) +void lofsMeshcoreSetHostFilesystem(Adafruit_LittleFS &filesystem) +{ + gInternal.bind(filesystem); +} +#elif LOBBS_ARCH_ESP32 +void lofsMeshcoreSetHostFilesystem(fs::SPIFFSFS &filesystem) +{ + gInternal.bind(filesystem); +} +#elif LOBBS_ARCH_RP2040 +void lofsMeshcoreSetHostFilesystem(LittleFSFS &filesystem) +{ + gInternal.bind(filesystem); +} +#else +void lofsMeshcoreSetHostFilesystem(void *filesystem) +{ + (void)filesystem; +} +#endif + +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}); +#endif +#if defined(EXTRAFS) && !defined(QSPIFLASH) + gSpare.bind(ExtraFS); + table.add({"spare", &gSpare, true, true}); +#endif + table.add({"internal", &gInternal, true, false}); +#elif LOBBS_ARCH_ESP32 + table.add({"internal", &gInternal, true, false}); +#elif LOBBS_ARCH_RP2040 + table.add({"internal", &gInternal, true, false}); +#endif +} + +#include "core/LoBBSStackGuard.h" +#endif diff --git a/lofs/platforms/meshcore/LoFSMountsMeshcore.h b/lofs/platforms/meshcore/LoFSMountsMeshcore.h new file mode 100644 index 0000000..beba7fc --- /dev/null +++ b/lofs/platforms/meshcore/LoFSMountsMeshcore.h @@ -0,0 +1,17 @@ +#pragma once +#include "core/LoBBSArch.h" + +#if defined(LOFS_NRF52) +class Adafruit_LittleFS; +void lofsMeshcoreSetHostFilesystem(Adafruit_LittleFS &filesystem); +#elif LOBBS_ARCH_ESP32 +namespace fs { +class SPIFFSFS; +} +void lofsMeshcoreSetHostFilesystem(fs::SPIFFSFS &filesystem); +#elif LOBBS_ARCH_RP2040 +class LittleFSFS; +void lofsMeshcoreSetHostFilesystem(LittleFSFS &filesystem); +#else +void lofsMeshcoreSetHostFilesystem(void *filesystem); +#endif diff --git a/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp b/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp new file mode 100644 index 0000000..b178dd6 --- /dev/null +++ b/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp @@ -0,0 +1,74 @@ +#if defined(LOBBS_PLATFORM_MESHTASTIC) +#include "lofs/volumes/LoFSVolume.h" +#include "core/LoBBSBootTrace.h" +#include "core/LoBBSConfig.h" +#include "configuration.h" +#include "FSCommon.h" +#include "SPILock.h" +#include "lofs/volumes/LoFSLittleFsVolume.h" +#include "lofs/volumes/LoFSArduinoFsVolume.h" +#include "lofs/volumes/LoFSNrfFlashBlockDevice.h" +#include "lofs/volumes/LoFSQspiNorBlockDevice.h" +#include "lofs/volumes/LoFSSdVolume.h" +#include + +#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 + +namespace { + +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) +LoFSSdVolume gSd; +#endif + +#if LOFS_BOARD_HAS_QSPI +LoFSQspiNorBlockDevice gQspiDevice; +LoFSLittleFsVolume gQspi(gQspiDevice); +#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 gInternal(FSCom); +#elif LOBBS_ARCH_PORTDUINO +LoFSArduinoFsVolume gInternal(FSCom); +#endif + +void addSpec(LoFSMountTable &table, const char *name, LoFSVolume *vol, bool shared, bool formattable) +{ + table.add({name, vol, shared, formattable}); +} + +} // namespace + +void lofsPlatformMounts(LoFSMountTable &table) +{ + LOBBS_BOOT_STEP("LoFS: meshtastic mounts"); + +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) + addSpec(table, "sd", &gSd, false, false); +#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); +#endif + + addSpec(table, "internal", &gInternal, true, true); +} + +#include "core/LoBBSStackGuard.h" +#endif diff --git a/lofs/platforms/native/LoFSMountsNative.cpp b/lofs/platforms/native/LoFSMountsNative.cpp new file mode 100644 index 0000000..9c5bbf0 --- /dev/null +++ b/lofs/platforms/native/LoFSMountsNative.cpp @@ -0,0 +1,10 @@ +#if defined(LOBBS_PLATFORM_NATIVE) +#include "lofs/volumes/LoFSVolume.h" + +void lofsPlatformMounts(LoFSMountTable &table) +{ + (void)table; +} + +#include "core/LoBBSStackGuard.h" +#endif diff --git a/lofs/volumes/LoFSArduinoFsVolume.cpp b/lofs/volumes/LoFSArduinoFsVolume.cpp new file mode 100644 index 0000000..ce91928 --- /dev/null +++ b/lofs/volumes/LoFSArduinoFsVolume.cpp @@ -0,0 +1,91 @@ +#include "LoFSArduinoFsVolume.h" + +#if LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040 || LOBBS_ARCH_PORTDUINO +#include + +namespace { + +struct FsFileHandle { + fs::File file; +}; + +void destroyFs(void *handle) +{ + delete static_cast(handle); +} + +bool validFs(void *handle) +{ + auto *h = static_cast(handle); + return h && h->file; +} + +size_t readFs(void *handle, uint8_t *buf, size_t len) +{ + return validFs(handle) ? static_cast(handle)->file.read(buf, len) : 0; +} + +size_t writeFs(void *handle, const uint8_t *buf, size_t len) +{ + return validFs(handle) ? static_cast(handle)->file.write(buf, len) : 0; +} + +bool seekFs(void *handle, uint32_t pos) +{ + return validFs(handle) && static_cast(handle)->file.seek(pos); +} + +uint32_t sizeFs(void *handle) +{ + return validFs(handle) ? (uint32_t)static_cast(handle)->file.size() : 0; +} + +bool isDirFs(void *handle) +{ + return validFs(handle) && static_cast(handle)->file.isDirectory(); +} + +const char *nameFs(void *handle) +{ + return validFs(handle) ? static_cast(handle)->file.name() : ""; +} + +void closeFs(void *handle) +{ + if (auto *h = static_cast(handle)) { + h->file.close(); + delete h; + } +} + +void flushFs(void *handle) +{ + if (validFs(handle)) + static_cast(handle)->file.flush(); +} + +void *openNextFs(void *dirHandle) +{ + auto *dir = static_cast(dirHandle); + if (!dir || !dir->file) + return nullptr; + fs::File next = dir->file.openNextFile(); + if (!next) + return nullptr; + auto *out = new FsFileHandle(); + out->file = next; + return out; +} + +} // namespace + +const LoFileOps kLoFSFsFileOps = {destroyFs, validFs, readFs, writeFs, seekFs, sizeFs, + isDirFs, nameFs, closeFs, flushFs, openNextFs}; + +#else + +const LoFileOps kLoFSFsFileOps = {}; + +#endif + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSArduinoFsVolume.h b/lofs/volumes/LoFSArduinoFsVolume.h new file mode 100644 index 0000000..d8854e5 --- /dev/null +++ b/lofs/volumes/LoFSArduinoFsVolume.h @@ -0,0 +1,106 @@ +#pragma once + +#include "core/LoBBSArch.h" +#include "LoFSVolume.h" +#include + +#if LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040 || LOBBS_ARCH_PORTDUINO +#include + +extern const LoFileOps kLoFSFsFileOps; + +template class LoFSArduinoFsVolume : public LoFSVolume { + public: + LoFSArduinoFsVolume() = default; + explicit LoFSArduinoFsVolume(FsType &fs) : fs_(&fs) {} + + void bind(FsType &fs) + { + fs_ = &fs; + ready_ = false; + } + + bool begin() override + { + ready_ = fs_ != nullptr; + return ready_; + } + + LoFile open(const char *relPath, const char *mode) override + { + if (!ready_ || !fs_) + return LoFile(); + return LoFileAdopt(&kLoFSFsFileOps, new fs::File(fs_->open(normalizeRel(relPath), mode))); + } + + LoFile open(const char *relPath, uint8_t mode) override + { + return open(relPath, mode ? "w" : "r"); + } + + bool exists(const char *relPath) override + { + return ready_ && fs_ && fs_->exists(normalizeRel(relPath)); + } + + bool mkdir(const char *relPath) override + { + return ready_ && fs_ && fs_->mkdir(normalizeRel(relPath)); + } + + bool remove(const char *relPath) override + { + return ready_ && fs_ && fs_->remove(normalizeRel(relPath)); + } + + bool rename(const char *oldPath, const char *newPath) override + { + return ready_ && fs_ && fs_->rename(normalizeRel(oldPath), normalizeRel(newPath)); + } + + bool rmdir(const char *relPath) override + { + return ready_ && fs_ && fs_->rmdir(normalizeRel(relPath)); + } + + uint64_t totalBytes() override + { +#if LOBBS_ARCH_PORTDUINO + return 0; +#else + return ready_ && fs_ ? fs_->totalBytes() : 0; +#endif + } + + uint64_t usedBytes() override + { +#if LOBBS_ARCH_PORTDUINO + return 0; +#else + return ready_ && fs_ ? fs_->usedBytes() : 0; +#endif + } + + bool format() override + { +#if LOBBS_ARCH_PORTDUINO + return false; +#else + return ready_ && fs_ && fs_->format(); +#endif + } + + void spaceHint(uint32_t *blockOut, uint32_t *slackOut) override + { + if (!blockOut || !slackOut) + return; + *blockOut = 4096; + *slackOut = 8192; + } + + private: + FsType *fs_ = nullptr; + bool ready_ = false; +}; + +#endif diff --git a/lofs/volumes/LoFSBlockDevice.h b/lofs/volumes/LoFSBlockDevice.h new file mode 100644 index 0000000..171857f --- /dev/null +++ b/lofs/volumes/LoFSBlockDevice.h @@ -0,0 +1,11 @@ +#pragma once + +struct lfs_config; + +/** Fills LittleFS block callbacks for a raw partition or external NOR. */ +class LoFSBlockDevice { + public: + virtual ~LoFSBlockDevice() = default; + virtual bool fill(lfs_config &cfg) = 0; + virtual bool prepareFormat() { return true; } +}; diff --git a/lofs/volumes/LoFSLittleFsVolume.cpp b/lofs/volumes/LoFSLittleFsVolume.cpp new file mode 100644 index 0000000..e0a7a3a --- /dev/null +++ b/lofs/volumes/LoFSLittleFsVolume.cpp @@ -0,0 +1,240 @@ +#include "LoFSLittleFsVolume.h" +#include +#include + +#if defined(LOFS_NRF52) + +using namespace Adafruit_LittleFS_Namespace; + +namespace { + +struct AdafruitFileHandle { + File file; + explicit AdafruitFileHandle(File f) : file(std::move(f)) {} +}; + +void destroyAdafruit(void *handle) +{ + delete static_cast(handle); +} + +bool validAdafruit(void *handle) +{ + auto *h = static_cast(handle); + return h && h->file; +} + +size_t readAdafruit(void *handle, uint8_t *buf, size_t len) +{ + return validAdafruit(handle) ? static_cast(handle)->file.read(buf, len) : 0; +} + +size_t writeAdafruit(void *handle, const uint8_t *buf, size_t len) +{ + return validAdafruit(handle) ? static_cast(handle)->file.write(buf, len) : 0; +} + +bool seekAdafruit(void *handle, uint32_t pos) +{ + return validAdafruit(handle) && static_cast(handle)->file.seek(pos); +} + +uint32_t sizeAdafruit(void *handle) +{ + return validAdafruit(handle) ? (uint32_t)static_cast(handle)->file.size() : 0; +} + +bool isDirAdafruit(void *handle) +{ + return validAdafruit(handle) && static_cast(handle)->file.isDirectory(); +} + +const char *nameAdafruit(void *handle) +{ + return validAdafruit(handle) ? static_cast(handle)->file.name() : ""; +} + +void closeAdafruit(void *handle) +{ + if (auto *h = static_cast(handle)) { + h->file.close(); + delete h; + } +} + +void flushAdafruit(void *handle) +{ + if (validAdafruit(handle)) + static_cast(handle)->file.flush(); +} + +void *openNextAdafruit(void *dirHandle) +{ + auto *dir = static_cast(dirHandle); + if (!dir || !dir->file) + return nullptr; + File next = dir->file.openNextFile(); + if (!next) + return nullptr; + return new AdafruitFileHandle(std::move(next)); +} + +} // namespace + +const LoFileOps kLoFSAdafruitLittleFsFileOps = { + destroyAdafruit, validAdafruit, readAdafruit, writeAdafruit, seekAdafruit, sizeAdafruit, + isDirAdafruit, nameAdafruit, closeAdafruit, flushAdafruit, openNextAdafruit, +}; + +static LoFile wrapAdafruitFile(File &&f) +{ + if (!f) + return LoFile(); + return LoFileAdopt(&kLoFSAdafruitLittleFsFileOps, new AdafruitFileHandle(std::move(f))); +} + +LoFSLittleFsVolume::LoFSLittleFsVolume(Adafruit_LittleFS &adopted) : kind_(Kind::Adopted), adopted_(&adopted) {} + +LoFSLittleFsVolume::LoFSLittleFsVolume(LoFSBlockDevice &device) : kind_(Kind::Owned), device_(&device) {} + +void LoFSLittleFsVolume::bind(Adafruit_LittleFS &fs) +{ + kind_ = Kind::Adopted; + adopted_ = &fs; + ready_ = false; +} + +Adafruit_LittleFS &LoFSLittleFsVolume::activeFs() +{ + return kind_ == Kind::Adopted ? *adopted_ : ownedVol_; +} + +Adafruit_LittleFS &LoFSLittleFsVolume::fs() +{ + return activeFs(); +} + +bool LoFSLittleFsVolume::begin() +{ + if (kind_ == Kind::Adopted) { + ready_ = adopted_ != nullptr; + return ready_; + } + ready_ = false; + if (!device_ || !device_->fill(cfg_)) + return false; + if (ownedVol_.begin()) + return ready_ = true; + if (!device_->prepareFormat()) + return false; + return ready_ = (ownedVol_.format() && ownedVol_.begin()); +} + +uint8_t LoFSLittleFsVolume::modeFromString(const char *mode) +{ + if (!mode || strcmp(mode, "r") == 0) + return 0; + return 1; +} + +LoFile LoFSLittleFsVolume::open(const char *relPath, const char *mode) +{ + if (!ready_) + return LoFile(); + return wrapAdafruitFile(activeFs().open(normalizeRel(relPath), modeFromString(mode))); +} + +LoFile LoFSLittleFsVolume::open(const char *relPath, uint8_t mode) +{ + if (!ready_) + return LoFile(); + return wrapAdafruitFile(activeFs().open(normalizeRel(relPath), mode)); +} + +bool LoFSLittleFsVolume::exists(const char *relPath) +{ + return ready_ && activeFs().exists(normalizeRel(relPath)); +} + +bool LoFSLittleFsVolume::mkdir(const char *relPath) +{ + return ready_ && activeFs().mkdir(normalizeRel(relPath)); +} + +bool LoFSLittleFsVolume::remove(const char *relPath) +{ + return ready_ && activeFs().remove(normalizeRel(relPath)); +} + +bool LoFSLittleFsVolume::rename(const char *oldPath, const char *newPath) +{ + return ready_ && activeFs().rename(normalizeRel(oldPath), normalizeRel(newPath)); +} + +bool LoFSLittleFsVolume::rmdir(const char *relPath) +{ + return ready_ && activeFs().rmdir(normalizeRel(relPath)); +} + +bool LoFSLittleFsVolume::littleFsBytes(Adafruit_LittleFS &fs, uint64_t &totalOut, uint64_t &usedOut) +{ + lfs_t *lfs = fs._getFS(); + const lfs_config *cfg = lfs ? lfs->cfg : nullptr; + if (!cfg) + return false; + totalOut = (uint64_t)cfg->block_size * cfg->block_count; + uint32_t blocks = 0; + fs._lockFS(); + int err = lfs_traverse( + lfs, + [](void *ctx, lfs_block_t block) { + (void)block; + (*(uint32_t *)ctx)++; + return 0; + }, + &blocks); + fs._unlockFS(); + if (err) + return false; + usedOut = (uint64_t)blocks * cfg->block_size; + return true; +} + +uint64_t LoFSLittleFsVolume::totalBytes() +{ + uint64_t total = 0; + uint64_t used = 0; + if (!ready_ || !littleFsBytes(activeFs(), total, used)) + return 0; + return total; +} + +uint64_t LoFSLittleFsVolume::usedBytes() +{ + uint64_t total = 0; + uint64_t used = 0; + if (!ready_ || !littleFsBytes(activeFs(), total, used)) + return 0; + return used; +} + +bool LoFSLittleFsVolume::format() +{ + if (!ready_) + return false; + if (device_ && !device_->prepareFormat()) + return false; + return activeFs().format() && activeFs().begin(); +} + +void LoFSLittleFsVolume::spaceHint(uint32_t *blockOut, uint32_t *slackOut) +{ + if (!blockOut || !slackOut) + return; + *blockOut = 128; + *slackOut = 512; +} + +#endif + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSLittleFsVolume.h b/lofs/volumes/LoFSLittleFsVolume.h new file mode 100644 index 0000000..3400e4d --- /dev/null +++ b/lofs/volumes/LoFSLittleFsVolume.h @@ -0,0 +1,51 @@ +#pragma once + +#include "core/LoBBSConfig.h" +#include "LoFSBlockDevice.h" +#include "LoFSVolume.h" + +#if defined(LOFS_NRF52) +#include + +class LoFSLittleFsVolume : public LoFSVolume { + public: + LoFSLittleFsVolume() = default; + explicit LoFSLittleFsVolume(Adafruit_LittleFS &adopted); + explicit LoFSLittleFsVolume(LoFSBlockDevice &device); + void bind(Adafruit_LittleFS &fs); + + bool begin() 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; + + Adafruit_LittleFS &fs(); + + private: + enum class Kind { Adopted, Owned }; + + Kind kind_; + Adafruit_LittleFS *adopted_ = nullptr; + LoFSBlockDevice *device_ = nullptr; + lfs_config cfg_{}; + Adafruit_LittleFS ownedVol_{&cfg_}; + bool ready_ = false; + Adafruit_LittleFS &activeFs(); + static uint8_t modeFromString(const char *mode); + static bool littleFsBytes(Adafruit_LittleFS &fs, uint64_t &totalOut, uint64_t &usedOut); +}; + +extern const LoFileOps kLoFSAdafruitLittleFsFileOps; + +#endif diff --git a/lofs/volumes/LoFSNrfFlashBlockDevice.cpp b/lofs/volumes/LoFSNrfFlashBlockDevice.cpp new file mode 100644 index 0000000..879af53 --- /dev/null +++ b/lofs/volumes/LoFSNrfFlashBlockDevice.cpp @@ -0,0 +1,162 @@ +#include "LoFSNrfFlashBlockDevice.h" +#include + +#if defined(LOFS_NRF52) + +#if defined(LOBBS_PLATFORM_MESHTASTIC) +#include "flash/flash_nrf5x.h" +#include +#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 +#include + +LoFSNrfFlashBlockDevice::LoFSNrfFlashBlockDevice(void *startSym, void *endSym, uint32_t blockSize, + bool lockWithInternalFs, bool pageEraseOnFormat, + uint32_t flashPageSize) + : startSym_(startSym), endSym_(endSym), blockSize_(blockSize), lockWithInternalFs_(lockWithInternalFs), + pageEraseOnFormat_(pageEraseOnFormat), flashPageSize_(flashPageSize) +{ +} + +void LoFSNrfFlashBlockDevice::lockFs() +{ +#if defined(LOBBS_PLATFORM_MESHTASTIC) + if (lockWithInternalFs_) + InternalFS._lockFS(); +#else + (void)lockWithInternalFs_; +#endif +} + +void LoFSNrfFlashBlockDevice::unlockFs() +{ +#if defined(LOBBS_PLATFORM_MESHTASTIC) + if (lockWithInternalFs_) + InternalFS._unlockFS(); +#endif +} + +static LoFSNrfFlashBlockDevice::BlockCtx *blockCtx(const lfs_config *c) +{ + return c ? static_cast(c->context) : nullptr; +} + +static int readCb(const lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) +{ + auto *ctx = blockCtx(c); + if (!ctx || !ctx->owner) + return LFS_ERR_IO; + const uint32_t blockSize = ctx->blockSize; + ctx->owner->lockFs(); + int n = flash_nrf5x_read(buffer, ctx->base + block * blockSize + off, size); + ctx->owner->unlockFs(); + return n > 0 ? 0 : LFS_ERR_IO; +} + +static int progCb(const lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) +{ + auto *ctx = blockCtx(c); + if (!ctx || !ctx->owner) + return LFS_ERR_IO; + const uint32_t blockSize = ctx->blockSize; + ctx->owner->lockFs(); + int n = flash_nrf5x_write(ctx->base + block * blockSize + off, buffer, size); + ctx->owner->unlockFs(); + return n > 0 ? 0 : LFS_ERR_IO; +} + +static int eraseCb(const lfs_config *c, lfs_block_t block) +{ + auto *ctx = blockCtx(c); + if (!ctx || !ctx->owner) + return LFS_ERR_IO; + const uint32_t blockSize = ctx->blockSize; + uint8_t ff[128]; + if (blockSize > sizeof(ff)) + return LFS_ERR_IO; + memset(ff, 0xFF, blockSize); + ctx->owner->lockFs(); + int n = flash_nrf5x_write(ctx->base + block * blockSize, ff, blockSize); + ctx->owner->unlockFs(); + return n > 0 ? 0 : LFS_ERR_IO; +} + +static int syncCb(const lfs_config *c) +{ + auto *ctx = blockCtx(c); + if (!ctx || !ctx->owner) + return LFS_ERR_IO; + ctx->owner->lockFs(); + flash_nrf5x_flush(); + ctx->owner->unlockFs(); + return 0; +} + +bool LoFSNrfFlashBlockDevice::fill(lfs_config &cfg) +{ + const uint32_t start = (uint32_t)startSym_; + const uint32_t end = (uint32_t)endSym_; + if (!start || end <= start || blockSize_ == 0) + return false; + rangeStart_ = start; + rangeEnd_ = end; + blockCtx_.owner = this; + blockCtx_.base = start; + blockCtx_.blockSize = blockSize_; + cfg.context = &blockCtx_; + cfg.read = readCb; + cfg.prog = progCb; + cfg.erase = eraseCb; + cfg.sync = syncCb; + cfg.read_size = blockSize_; + cfg.prog_size = blockSize_; + cfg.block_size = blockSize_; + cfg.block_count = (end - start) / blockSize_; + cfg.lookahead = 128; + return cfg.block_count > 0; +} + +bool LoFSNrfFlashBlockDevice::prepareFormat() +{ + return eraseAll(); +} + +bool LoFSNrfFlashBlockDevice::eraseAll() +{ + if (!rangeStart_ || rangeEnd_ <= rangeStart_ || !pageEraseOnFormat_) + return true; + lockFs(); + flash_nrf5x_flush(); + for (uint32_t addr = rangeStart_; addr < rangeEnd_; addr += flashPageSize_) + flash_nrf5x_erase(addr); + unlockFs(); + return true; +} + +#else + +LoFSNrfFlashBlockDevice::LoFSNrfFlashBlockDevice(void *, void *, uint32_t, bool, bool, uint32_t) {} + +bool LoFSNrfFlashBlockDevice::fill(lfs_config &cfg) +{ + (void)cfg; + return false; +} + +bool LoFSNrfFlashBlockDevice::prepareFormat() +{ + return false; +} + +bool LoFSNrfFlashBlockDevice::eraseAll() +{ + return false; +} + +#endif + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSNrfFlashBlockDevice.h b/lofs/volumes/LoFSNrfFlashBlockDevice.h new file mode 100644 index 0000000..3fc6293 --- /dev/null +++ b/lofs/volumes/LoFSNrfFlashBlockDevice.h @@ -0,0 +1,35 @@ +#pragma once + +#include "LoFSBlockDevice.h" +#include + +class LoFSNrfFlashBlockDevice : public LoFSBlockDevice { + public: + LoFSNrfFlashBlockDevice(void *startSym, void *endSym, uint32_t blockSize, bool lockWithInternalFs, + bool pageEraseOnFormat, uint32_t flashPageSize = 4096); + + bool fill(lfs_config &cfg) override; + bool prepareFormat() override; + bool eraseAll(); + + void lockFs(); + void unlockFs(); + + struct BlockCtx { + LoFSNrfFlashBlockDevice *owner; + uint32_t base; + uint32_t blockSize; + }; + + private: + void *startSym_; + void *endSym_; + uint32_t blockSize_; + bool lockWithInternalFs_; + bool pageEraseOnFormat_; + uint32_t flashPageSize_; + uint32_t rangeStart_ = 0; + uint32_t rangeEnd_ = 0; + + BlockCtx blockCtx_{}; +}; diff --git a/lofs/LoFSQspi.cpp b/lofs/volumes/LoFSQspiNorBlockDevice.cpp similarity index 78% rename from lofs/LoFSQspi.cpp rename to lofs/volumes/LoFSQspiNorBlockDevice.cpp index 73286b2..1db5abb 100644 --- a/lofs/LoFSQspi.cpp +++ b/lofs/volumes/LoFSQspiNorBlockDevice.cpp @@ -1,18 +1,15 @@ -#include "LoFSQspi.h" -#include "configuration.h" - -#if LOBBS_EXTRA_QSPI && defined(ARCH_NRF52) -#include "nrf.h" -#include "variant.h" -#include +#include "LoFSQspiNorBlockDevice.h" #include + +#if LOFS_BOARD_HAS_QSPI +#include "variant.h" +#include "nrf.h" #include #include extern const uint32_t g_ADigitalPinMap[]; -#include "../LoBBSBootTrace.h" -#include "LoBBSStackGuard.h" +#include static constexpr uint32_t LOFS_QSPI_BLOCK = 4096; static constexpr uint32_t LOFS_QSPI_PAGE = 256; @@ -29,13 +26,11 @@ static uint8_t lofsQspiPin(uint32_t dPin) static nrfx_err_t lofsQspiCinstr(uint8_t op, nrf_qspi_cinstr_len_t len, void *rx) { nrf_qspi_cinstr_conf_t c = NRFX_QSPI_DEFAULT_CINSTR(op, len); - // IO2/IO3 are WP#/HOLD# when the quad-enable bit is clear; low HOLD# makes the flash ignore the command. c.io2_level = true; c.io3_level = true; return nrfx_qspi_cinstr_xfer(&c, NULL, rx); } -// nrfx_qspi_mem_busy_check() sends RDSR with HOLD# low, so the status byte floats. static bool lofsQspiWaitReady() { uint8_t sr[4]; @@ -48,16 +43,14 @@ static bool lofsQspiWaitReady() static bool lofsQspiReadJedec(uint8_t out[3]) { - // Flash state survives MCU resets on battery: release deep power-down, then software reset (66h/99h). - for (uint8_t op : {0xAB, 0x66, 0x99}) { - lofsQspiCinstr(op, NRF_QSPI_CINSTR_LEN_1B, NULL); + const uint8_t wakeOps[] = {0xAB, 0x66, 0x99}; + for (size_t i = 0; i < sizeof(wakeOps); i++) { + lofsQspiCinstr(wakeOps[i], NRF_QSPI_CINSTR_LEN_1B, NULL); delayMicroseconds(50); } - - LOBBS_BOOT_STEP("qspi: jedec cinstr xfer"); nrfx_err_t err = lofsQspiCinstr(0x9F, NRF_QSPI_CINSTR_LEN_4B, out); if (err != NRFX_SUCCESS) { - LOG_ERROR("LoFS QSPI JEDEC cinstr err %d", (int)err); + Serial.printf("LoFS QSPI JEDEC cinstr err %d\n", (int)err); return false; } return true; @@ -68,8 +61,6 @@ static bool lofsQspiInitHw() if (lofsQspiHwReady) return true; - LOBBS_BOOT_STEP("qspi: init hw enter"); - #ifndef NRFX_QSPI_DEFAULT_CONFIG_IRQ_PRIORITY #define NRFX_QSPI_DEFAULT_CONFIG_IRQ_PRIORITY 6 #endif @@ -77,15 +68,13 @@ static bool lofsQspiInitHw() nrfx_qspi_config_t cfg = NRFX_QSPI_DEFAULT_CONFIG(lofsQspiPin(PIN_QSPI_SCK), lofsQspiPin(PIN_QSPI_CS), lofsQspiPin(PIN_QSPI_IO0), lofsQspiPin(PIN_QSPI_IO1), lofsQspiPin(PIN_QSPI_IO2), lofsQspiPin(PIN_QSPI_IO3)); - // FASTREAD default: works without QE bit; READ2IO needs status-register setup on P25Q16. cfg.phy_if.sck_freq = NRF_QSPI_FREQ_DIV8; - LOBBS_BOOT_STEP("qspi: nrfx_qspi_init"); nrfx_err_t err = nrfx_qspi_init(&cfg, NULL, NULL); if (err == NRFX_ERROR_INVALID_STATE) { lofsQspiHwReady = true; } else if (err != NRFX_SUCCESS) { - LOG_ERROR("LoFS QSPI init failed: %d", (int)err); + Serial.printf("LoFS QSPI init failed: %d\n", (int)err); return false; } else { lofsQspiHwReady = true; @@ -93,14 +82,14 @@ static bool lofsQspiInitHw() uint8_t jedec[3]; if (!lofsQspiReadJedec(jedec)) { - LOG_ERROR("LoFS QSPI JEDEC read failed"); + Serial.printf("LoFS QSPI JEDEC read failed\n"); nrfx_qspi_uninit(); lofsQspiHwReady = false; return false; } - LOG_INFO("LoFS QSPI JEDEC %02x %02x %02x", jedec[0], jedec[1], jedec[2]); - if (jedec[0] != 0x85) { - LOG_ERROR("LoFS QSPI unexpected manufacturer 0x%02x", jedec[0]); + Serial.printf("LoFS QSPI JEDEC %02x %02x %02x\n", jedec[0], jedec[1], jedec[2]); + if (jedec[0] != 0x85 && jedec[0] != 0x9D) { + Serial.printf("LoFS QSPI unexpected manufacturer 0x%02x\n", jedec[0]); nrfx_qspi_uninit(); lofsQspiHwReady = false; return false; @@ -131,7 +120,8 @@ static int lofsQspiRead(const struct lfs_config *c, lfs_block_t block, lfs_off_t return 0; } -static int lofsQspiProg(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) +static int lofsQspiProg(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, + lfs_size_t size) { (void)c; uint32_t addr = block * LOFS_QSPI_BLOCK + off; @@ -174,9 +164,8 @@ static int lofsQspiSync(const struct lfs_config *c) return 0; } -bool lofsQspiConfig(struct lfs_config &cfg) +bool LoFSQspiNorBlockDevice::fill(lfs_config &cfg) { - LOBBS_BOOT_STEP("qspi: lofsQspiConfig"); if (!lofsQspiInitHw()) return false; cfg.context = NULL; @@ -192,4 +181,14 @@ bool lofsQspiConfig(struct lfs_config &cfg) return true; } +#else + +bool LoFSQspiNorBlockDevice::fill(lfs_config &cfg) +{ + (void)cfg; + return false; +} + #endif + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSQspiNorBlockDevice.h b/lofs/volumes/LoFSQspiNorBlockDevice.h new file mode 100644 index 0000000..b703c57 --- /dev/null +++ b/lofs/volumes/LoFSQspiNorBlockDevice.h @@ -0,0 +1,18 @@ +#pragma once + +#include "core/LoBBSConfig.h" +#include "LoFSBlockDevice.h" + +#if !defined(LOFS_BOARD_HAS_QSPI) +#if ((defined(LOFS_ENABLE_EXTRA_QSPI) && LOFS_ENABLE_EXTRA_QSPI) || defined(QSPIFLASH)) && \ + defined(LOFS_NRF52) +#define LOFS_BOARD_HAS_QSPI 1 +#else +#define LOFS_BOARD_HAS_QSPI 0 +#endif +#endif + +class LoFSQspiNorBlockDevice : public LoFSBlockDevice { + public: + bool fill(lfs_config &cfg) override; +}; diff --git a/lofs/volumes/LoFSSdVolume.cpp b/lofs/volumes/LoFSSdVolume.cpp new file mode 100644 index 0000000..ad538ba --- /dev/null +++ b/lofs/volumes/LoFSSdVolume.cpp @@ -0,0 +1,294 @@ +#include "LoFSSdVolume.h" +#include "core/LoBBSArch.h" +#include + +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) +#include +#include +#ifdef SDCARD_USE_SPI1 +extern SPIClass SPI_HSPI; +#define SDHandler SPI_HSPI +#else +#define SDHandler SPI +#endif +#ifndef SD_SPI_FREQUENCY +#define SD_SPI_FREQUENCY 4000000U +#endif + +#if !defined(FILE_O_READ) && !LOBBS_ARCH_PORTDUINO +#define FILE_O_READ 0 +#define FILE_O_WRITE 1 +#endif + +namespace { + +struct SdFileHandle { + File file; +}; + +void destroySd(void *handle) +{ + delete static_cast(handle); +} + +bool validSd(void *handle) +{ + auto *h = static_cast(handle); + return h && h->file; +} + +size_t readSd(void *handle, uint8_t *buf, size_t len) +{ + return validSd(handle) ? static_cast(handle)->file.read(buf, len) : 0; +} + +size_t writeSd(void *handle, const uint8_t *buf, size_t len) +{ + return validSd(handle) ? static_cast(handle)->file.write(buf, len) : 0; +} + +bool seekSd(void *handle, uint32_t pos) +{ + return validSd(handle) && static_cast(handle)->file.seek(pos); +} + +uint32_t sizeSd(void *handle) +{ + return validSd(handle) ? (uint32_t)static_cast(handle)->file.size() : 0; +} + +bool isDirSd(void *handle) +{ + return validSd(handle) && static_cast(handle)->file.isDirectory(); +} + +const char *nameSd(void *handle) +{ + return validSd(handle) ? static_cast(handle)->file.name() : ""; +} + +void closeSd(void *handle) +{ + if (auto *h = static_cast(handle)) { + h->file.close(); + delete h; + } +} + +void flushSd(void *handle) +{ + if (validSd(handle)) + static_cast(handle)->file.flush(); +} + +void *openNextSd(void *dirHandle) +{ + auto *dir = static_cast(dirHandle); + if (!dir || !dir->file) + return nullptr; + File next = dir->file.openNextFile(); + if (!next) + return nullptr; + auto *out = new SdFileHandle(); + out->file = next; + return out; +} + +#if LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040 || LOBBS_ARCH_PORTDUINO +static const char *sdModeStr(const char *mode) +{ + return mode; +} +#else +static uint8_t sdModeFromStr(const char *mode) +{ + return (mode && strcmp(mode, "r") == 0) ? FILE_READ : FILE_WRITE; +} +#endif + +} // namespace + +const LoFileOps kLoFSSdFileOps = {destroySd, validSd, readSd, writeSd, seekSd, sizeSd, + isDirSd, nameSd, closeSd, flushSd, openNextSd}; + +static bool lobfsSdPresent() +{ + uint8_t cardType = SD.cardType(); + if (cardType == CARD_NONE) { + SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + if (SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) + cardType = SD.cardType(); + } + return cardType != CARD_NONE; +} + +const char *LoFSSdVolume::sdPath(const char *rel) +{ + if (!rel || rel[0] == '\0') + return "/"; + if (rel[0] == '/') + return rel + 1; + return rel; +} + +bool LoFSSdVolume::begin() +{ + ready_ = lobfsSdPresent(); + return ready_; +} + +LoFile LoFSSdVolume::open(const char *relPath, const char *mode) +{ + if (!ready_) + return LoFile(); +#if LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040 || LOBBS_ARCH_PORTDUINO + File f = SD.open(sdPath(relPath), sdModeStr(mode)); +#else + File f = SD.open(sdPath(relPath), sdModeFromStr(mode)); +#endif + if (!f) + return LoFile(); + auto *h = new SdFileHandle(); + h->file = std::move(f); + return LoFileAdopt(&kLoFSSdFileOps, h); +} + +LoFile LoFSSdVolume::open(const char *relPath, uint8_t mode) +{ +#if LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040 || LOBBS_ARCH_PORTDUINO + return open(relPath, mode ? "w" : "r"); +#else + if (!ready_) + return LoFile(); + File f = SD.open(sdPath(relPath), mode ? FILE_WRITE : FILE_READ); + if (!f) + return LoFile(); + auto *h = new SdFileHandle(); + h->file = std::move(f); + return LoFileAdopt(&kLoFSSdFileOps, h); +#endif +} + +bool LoFSSdVolume::exists(const char *relPath) +{ + return ready_ && SD.exists(sdPath(relPath)); +} + +bool LoFSSdVolume::mkdir(const char *relPath) +{ + return ready_ && SD.mkdir(sdPath(relPath)); +} + +bool LoFSSdVolume::remove(const char *relPath) +{ + return ready_ && SD.remove(sdPath(relPath)); +} + +bool LoFSSdVolume::rename(const char *oldPath, const char *newPath) +{ + return ready_ && SD.rename(sdPath(oldPath), sdPath(newPath)); +} + +bool LoFSSdVolume::rmdir(const char *relPath) +{ + return ready_ && SD.rmdir(sdPath(relPath)); +} + +uint64_t LoFSSdVolume::totalBytes() +{ + return ready_ ? SD.totalBytes() : 0; +} + +uint64_t LoFSSdVolume::usedBytes() +{ + return ready_ ? SD.usedBytes() : 0; +} + +bool LoFSSdVolume::format() +{ + return false; +} + +void LoFSSdVolume::spaceHint(uint32_t *blockOut, uint32_t *slackOut) +{ + if (!blockOut || !slackOut) + return; + *blockOut = 512; + *slackOut = 64 * 1024; +} + +#else + +const LoFileOps kLoFSSdFileOps = {}; + +const char *LoFSSdVolume::sdPath(const char *rel) +{ + return rel; +} + +bool LoFSSdVolume::begin() +{ + return false; +} + +LoFile LoFSSdVolume::open(const char *, const char *) +{ + return LoFile(); +} + +LoFile LoFSSdVolume::open(const char *, uint8_t) +{ + return LoFile(); +} + +bool LoFSSdVolume::exists(const char *) +{ + return false; +} + +bool LoFSSdVolume::mkdir(const char *) +{ + return false; +} + +bool LoFSSdVolume::remove(const char *) +{ + return false; +} + +bool LoFSSdVolume::rename(const char *, const char *) +{ + return false; +} + +bool LoFSSdVolume::rmdir(const char *) +{ + return false; +} + +uint64_t LoFSSdVolume::totalBytes() +{ + return 0; +} + +uint64_t LoFSSdVolume::usedBytes() +{ + return 0; +} + +bool LoFSSdVolume::format() +{ + return false; +} + +void LoFSSdVolume::spaceHint(uint32_t *blockOut, uint32_t *slackOut) +{ + if (blockOut) + *blockOut = 512; + if (slackOut) + *slackOut = 0; +} + +#endif + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSSdVolume.h b/lofs/volumes/LoFSSdVolume.h new file mode 100644 index 0000000..92e7270 --- /dev/null +++ b/lofs/volumes/LoFSSdVolume.h @@ -0,0 +1,28 @@ +#pragma once + +#include "LoFSVolume.h" + +class LoFSSdVolume : public LoFSVolume { + public: + bool begin() 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: + bool ready_ = false; + static const char *sdPath(const char *rel); +}; + +extern const LoFileOps kLoFSSdFileOps; diff --git a/lofs/volumes/LoFSVolume.cpp b/lofs/volumes/LoFSVolume.cpp new file mode 100644 index 0000000..1b62e9f --- /dev/null +++ b/lofs/volumes/LoFSVolume.cpp @@ -0,0 +1,38 @@ +#include "LoFSVolume.h" +#include "core/LoBBSArch.h" +#include + +const char *LoFSVolume::normalizeRel(const char *rel) +{ + if (!rel || rel[0] == '\0') + return "/"; + return rel; +} + +uint32_t lofsPlatformSharedReserve(const char *mountName) +{ + (void)mountName; +#if LOBBS_ARCH_NRF52 + return 16 * 1024; +#elif LOBBS_ARCH_PORTDUINO + return 0; +#else + return 128 * 1024; +#endif +} + +bool LoFSMountTable::add(const LoFSMountSpec &spec) +{ + if (!spec.name || !spec.volume || count >= LOFS_MAX_MOUNTS) + return false; + for (int i = 0; i < count; i++) { + if (strcmp(specs[i].name, spec.name) == 0) { + specs[i] = spec; + return true; + } + } + specs[count++] = spec; + return true; +} + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSVolume.h b/lofs/volumes/LoFSVolume.h new file mode 100644 index 0000000..df4cffd --- /dev/null +++ b/lofs/volumes/LoFSVolume.h @@ -0,0 +1,51 @@ +#pragma once + +#include "../LoFile.h" +#include +#include + +static constexpr const char *LOFS_MOUNT_ORDER[] = {"sd", "qspi", "reserve", "spare", "internal"}; +static constexpr int LOFS_MOUNT_ORDER_LEN = 5; +static constexpr int LOFS_MAX_MOUNTS = 6; + +class LoFSVolume { + public: + virtual ~LoFSVolume() = default; + + /** Probe and mount if needed. False when this slot is absent on this hardware. */ + virtual bool begin() = 0; + + virtual LoFile open(const char *relPath, const char *mode) = 0; + virtual LoFile open(const char *relPath, uint8_t mode) = 0; + + virtual bool exists(const char *relPath) = 0; + virtual bool mkdir(const char *relPath) = 0; + virtual bool remove(const char *relPath) = 0; + virtual bool rename(const char *oldPath, const char *newPath) = 0; + virtual bool rmdir(const char *relPath) = 0; + + virtual uint64_t totalBytes() = 0; + virtual uint64_t usedBytes() = 0; + virtual bool format() = 0; + virtual void spaceHint(uint32_t *blockOut, uint32_t *slackOut) = 0; + + static const char *normalizeRel(const char *rel); +}; + +struct LoFSMountSpec { + const char *name; + LoFSVolume *volume; + bool shared; + bool formattable; +}; + +struct LoFSMountTable { + LoFSMountSpec specs[LOFS_MAX_MOUNTS]; + int count = 0; + + bool add(const LoFSMountSpec &spec); +}; + +void lofsPlatformMounts(LoFSMountTable &table); + +uint32_t lofsPlatformSharedReserve(const char *mountName); diff --git a/loscalar/LoScalar.cpp b/loscalar/LoScalar.cpp index da4f588..2b6046a 100644 --- a/loscalar/LoScalar.cpp +++ b/loscalar/LoScalar.cpp @@ -4,7 +4,7 @@ #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static constexpr uint32_t kLoScalarFieldMax = 99; diff --git a/platforms/LoPlatform.h b/platforms/LoPlatform.h new file mode 100644 index 0000000..55a0340 --- /dev/null +++ b/platforms/LoPlatform.h @@ -0,0 +1,47 @@ +#pragma once +#include "core/LoWire.h" + +#if !defined(LOBBS_PLATFORM_MESHTASTIC) && !defined(LOBBS_PLATFORM_MESHCORE) && !defined(LOBBS_PLATFORM_NATIVE) +#error Define LOBBS_PLATFORM_MESHTASTIC, LOBBS_PLATFORM_MESHCORE, or LOBBS_PLATFORM_NATIVE for this build +#endif + +// Lifecycle (PAL contract): +// 1. Construct LoBBSKernel (no FS or hooks yet). +// 2. Host binds filesystem / mesh / RTC (MeshCore: before lobbsMeshCoreInit returns). +// 3. kernel.begin() mounts LoFS, runs wireup, and runs install init. +// 4. Only then may the host call lobbsCoreHandleInbound or platform DM/serial helpers. +// replyContext is valid only for the duration of a single inbound handler call. + +#include +#include + +enum class LoPlatformLogLevel : uint8_t { Debug, Info, Warn, Error }; + +enum class LoPlatformSetUnixResult : uint8_t { Ok, Invalid, Failed }; + +uint32_t lobbsPlatformMillis(); +uint32_t lobbsPlatformUnixNow(); +LoPlatformSetUnixResult lobbsPlatformSetUnix(uint32_t unixSec); +const char *lobbsPlatformClockName(); + +/** Returns a value in [0, bound). */ +uint32_t lobbsPlatformRandom(uint32_t bound); + +void lobbsPlatformLog(LoPlatformLogLevel level, const char *fmt, ...); + +/** Host-specific work after a mount format (e.g. persist radio DB). */ +void lobbsPlatformAfterFormat(const char *mount); + +/** Optional stack headroom warning from the inbound handler path. */ +void lobbsPlatformNoteHandlerStack(); + +uint32_t lobbsPlatformLocalNodeId(); +/** Max bytes per outbound text frame (paginators target this). */ +size_t lobbsPlatformMaxReplyBytes(); +void lobbsPlatformSendReply(const LoInbound &inbound, const char *msg); +bool lobbsPlatformInstallAuthorized(const LoInbound &inbound); + +#define LOBBS_LOG_DEBUG(fmt, ...) lobbsPlatformLog(LoPlatformLogLevel::Debug, fmt, ##__VA_ARGS__) +#define LOBBS_LOG_INFO(fmt, ...) lobbsPlatformLog(LoPlatformLogLevel::Info, fmt, ##__VA_ARGS__) +#define LOBBS_LOG_WARN(fmt, ...) lobbsPlatformLog(LoPlatformLogLevel::Warn, fmt, ##__VA_ARGS__) +#define LOBBS_LOG_ERROR(fmt, ...) lobbsPlatformLog(LoPlatformLogLevel::Error, fmt, ##__VA_ARGS__) diff --git a/platforms/arduino/LoPlatformArduino.cpp b/platforms/arduino/LoPlatformArduino.cpp new file mode 100644 index 0000000..732b266 --- /dev/null +++ b/platforms/arduino/LoPlatformArduino.cpp @@ -0,0 +1,24 @@ +#if defined(LOBBS_PLATFORM_MESHTASTIC) || defined(LOBBS_PLATFORM_MESHCORE) +#include "platforms/LoPlatform.h" +#include "platforms/arduino/LoPlatformArduino.h" +#include +#include + +void lobbsPlatformLogFormat(char *buf, size_t cap, const char *fmt, va_list args) +{ + vsnprintf(buf, cap, fmt, args); +} + +uint32_t lobbsPlatformMillis() +{ + return millis(); +} + +uint32_t lobbsPlatformRandom(uint32_t bound) +{ + if (bound == 0) + return 0; + return (uint32_t)random(bound); +} + +#endif diff --git a/platforms/arduino/LoPlatformArduino.h b/platforms/arduino/LoPlatformArduino.h new file mode 100644 index 0000000..8e4bd69 --- /dev/null +++ b/platforms/arduino/LoPlatformArduino.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +#if defined(LOBBS_PLATFORM_MESHTASTIC) || defined(LOBBS_PLATFORM_MESHCORE) + +void lobbsPlatformLogFormat(char *buf, size_t cap, const char *fmt, va_list args); + +#endif diff --git a/platforms/extra_script.py b/platforms/extra_script.py new file mode 100644 index 0000000..f7a4fdb --- /dev/null +++ b/platforms/extra_script.py @@ -0,0 +1,72 @@ +Import("env") +import os + +try: + Import("pio_lib_builder") +except Exception: + pio_lib_builder = None + +try: + Import("projenv") +except Exception: + projenv = None + + +def _read_lobbs_version(version_path): + major, minor, build = 2, 0, 0 + if not os.path.isfile(version_path): + return None, None + with open(version_path) as f: + in_section = False + for line in f: + line = line.strip() + if line == "[LOBBS]": + in_section = True + continue + if line.startswith("[") and line.endswith("]"): + in_section = False + continue + if not in_section: + continue + if line.startswith("major="): + major = int(line.split("=", 1)[1]) + elif line.startswith("minor="): + minor = int(line.split("=", 1)[1]) + elif line.startswith("build="): + build = int(line.split("=", 1)[1]) + long_ver = f"{major}.{minor}.{build}" + short_ver = f"{major}.{minor}" + return long_ver, short_ver + + +def _lobbs_version_path(): + if pio_lib_builder is not None: + return os.path.join(pio_lib_builder.path, "version.properties") + project_dir = env.subst("$PROJECT_DIR") + for root in ( + project_dir, + os.path.normpath(os.path.join(project_dir, "..", "..")), + ): + candidate = os.path.join(root, "version.properties") + if os.path.isfile(candidate): + return candidate + return os.path.join(project_dir, "version.properties") + + +def _inject_version(e): + if e is None: + return + version_path = _lobbs_version_path() + long_ver, short_ver = _read_lobbs_version(version_path) + if not long_ver: + return + e.Append( + CPPDEFINES=[ + ("LOBBS_VERSION", '\\"' + long_ver + '\\"'), + ("LOBBS_VERSION_SHORT", '\\"' + short_ver + '\\"'), + ] + ) + + +_inject_version(env) +_inject_version(projenv) diff --git a/platforms/meshcore/LoPlatformMeshcore.cpp b/platforms/meshcore/LoPlatformMeshcore.cpp new file mode 100644 index 0000000..bba5b7f --- /dev/null +++ b/platforms/meshcore/LoPlatformMeshcore.cpp @@ -0,0 +1,256 @@ +#if defined(LOBBS_PLATFORM_MESHCORE) +#include "core/LoBBSArch.h" +#include +#if LOBBS_ARCH_ESP32 +#include +#endif +#if LOBBS_ARCH_RP2040 +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +#include "platforms/LoPlatform.h" +#include "platforms/arduino/LoPlatformArduino.h" +#include "core/LoBBSDispatch.h" +#include "core/LoBBSKernel.h" +#include "platforms/meshcore/LoPlatformMeshcore.h" +#include "lofs/platforms/meshcore/LoFSMountsMeshcore.h" + +static LoBBSKernel *gCore = nullptr; +static BaseChatMesh *gMesh = nullptr; +static uint32_t gLocalNodeId = 0; +static mesh::RTCClock *gRtc = nullptr; + +static uint32_t lobbsHashPubKey(const uint8_t *pub, size_t len) +{ + uint32_t h = 2166136261u; + for (size_t i = 0; i < len; i++) { + h ^= pub[i]; + h *= 16777619u; + } + return h; +} + +static uint32_t lobbsMeshUnixNow() +{ + return gRtc ? gRtc->getCurrentTime() : 0; +} + +uint32_t lobbsPlatformUnixNow() +{ + return lobbsMeshUnixNow(); +} + +LoPlatformSetUnixResult lobbsPlatformSetUnix(uint32_t unixSec) +{ + if (!gRtc) + return LoPlatformSetUnixResult::Failed; + if (unixSec < 946684800u) + return LoPlatformSetUnixResult::Invalid; + gRtc->setCurrentTime(unixSec); + return LoPlatformSetUnixResult::Ok; +} + +const char *lobbsPlatformClockName() +{ + return gRtc ? "device" : "none"; +} + +void lobbsPlatformLog(LoPlatformLogLevel level, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + const char *tag = "DEBUG"; + switch (level) { + case LoPlatformLogLevel::Debug: + tag = "DEBUG"; + break; + case LoPlatformLogLevel::Info: + tag = "INFO "; + break; + case LoPlatformLogLevel::Warn: + tag = "WARN "; + break; + case LoPlatformLogLevel::Error: + tag = "ERROR"; + break; + } + Serial.printf("%s | LoBBS ", tag); + char buf[256]; + lobbsPlatformLogFormat(buf, sizeof(buf), fmt, args); + va_end(args); + Serial.println(buf); +} + +void lobbsPlatformAfterFormat(const char *mount) +{ + (void)mount; +} + +void lobbsPlatformNoteHandlerStack() +{ +} + +uint32_t lobbsPlatformLocalNodeId() +{ + return gLocalNodeId; +} + +size_t lobbsPlatformMaxReplyBytes() +{ + return MAX_TEXT_LEN; +} + +#if defined(LOFS_NRF52) +#include +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, Adafruit_LittleFS &filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock) +{ + gCore = core; + gMesh = &mesh; + lofsMeshcoreSetHostFilesystem(filesystem); + if (selfPubKey32) + gLocalNodeId = lobbsHashPubKey(selfPubKey32, PUB_KEY_SIZE); + gRtc = &rtcClock; + if (core) + core->begin(); +} +#elif LOBBS_ARCH_ESP32 +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, fs::SPIFFSFS &filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock) +{ + gCore = core; + gMesh = &mesh; + lofsMeshcoreSetHostFilesystem(filesystem); + if (selfPubKey32) + gLocalNodeId = lobbsHashPubKey(selfPubKey32, PUB_KEY_SIZE); + gRtc = &rtcClock; + if (core) + core->begin(); +} +#elif LOBBS_ARCH_RP2040 +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, LittleFSFS &filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock) +{ + gCore = core; + gMesh = &mesh; + lofsMeshcoreSetHostFilesystem(filesystem); + if (selfPubKey32) + gLocalNodeId = lobbsHashPubKey(selfPubKey32, PUB_KEY_SIZE); + gRtc = &rtcClock; + if (core) + core->begin(); +} +#else +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, void *filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock) +{ + gCore = core; + gMesh = &mesh; + lofsMeshcoreSetHostFilesystem(filesystem); + if (selfPubKey32) + gLocalNodeId = lobbsHashPubKey(selfPubKey32, PUB_KEY_SIZE); + gRtc = &rtcClock; + if (core) + core->begin(); +} +#endif + +void lobbsMeshCoreLoop(LoBBSKernel *core) +{ + (void)core; +} + +static void lobbsFillInbound(LoInbound &in, const ContactInfo &from, const char *text) +{ + in.from.key = lobbsHashPubKey(from.id.pub_key, PUB_KEY_SIZE); + in.channel = LoInboundChannel::Radio; + in.text = text; + in.replyContext = &from; +} + +bool lobbsMeshCoreHandleDm(LoBBSKernel *core, const void *contact, const char *text) +{ + if (!core || !contact || !text || !text[0]) + return false; + + const ContactInfo &from = *static_cast(contact); + LoInbound in; + lobbsFillInbound(in, from, text); + + if (text[0] != '/') + return false; + lobbsCoreHandleInbound(core, in); + return true; +} + +bool lobbsMeshCoreDeliverMail(LoBBSKernel *core, const void *contact, const char *text) +{ + if (!core || !contact || !text || !text[0] || text[0] == '/') + return false; + const ContactInfo &from = *static_cast(contact); + LoInbound in; + lobbsFillInbound(in, from, text); + lobbsCoreDeliverUserMail(core, in); + return true; +} + +bool lobbsMeshCoreHandleSerialLine(LoBBSKernel *core, const char *line) +{ + if (!core || !line || !line[0]) + return false; + if (line[0] != '/') + return false; + + LoInbound in; + in.from.key = lobbsPlatformLocalNodeId(); + in.channel = LoInboundChannel::Local; + in.text = line; + in.replyContext = nullptr; + lobbsCoreHandleInbound(core, in); + return true; +} + +static void lobbsSendChunk(const ContactInfo &to, const char *chunk, size_t len) +{ + if (!gMesh || !chunk || len == 0) + return; + char buf[MAX_TEXT_LEN + 1]; + if (len > MAX_TEXT_LEN) + len = MAX_TEXT_LEN; + memcpy(buf, chunk, len); + buf[len] = '\0'; + + uint32_t expected_ack = 0; + uint32_t est_timeout = 0; + (void)gMesh->sendMessage(to, lobbsMeshUnixNow(), 0, buf, expected_ack, est_timeout); +} + +void lobbsPlatformSendReply(const LoInbound &inbound, const char *msg) +{ + if (!msg) + return; + if (!inbound.replyContext) { + if (inbound.channel == LoInboundChannel::Local) + Serial.println(msg); + return; + } + const ContactInfo &to = *static_cast(inbound.replyContext); + size_t len = strlen(msg); + size_t maxBytes = lobbsPlatformMaxReplyBytes(); + if (len > maxBytes) + len = maxBytes; + lobbsSendChunk(to, msg, len); +} + +bool lobbsPlatformInstallAuthorized(const LoInbound &inbound) +{ + return inbound.channel == LoInboundChannel::Local; +} +#endif diff --git a/platforms/meshcore/LoPlatformMeshcore.h b/platforms/meshcore/LoPlatformMeshcore.h new file mode 100644 index 0000000..63bf303 --- /dev/null +++ b/platforms/meshcore/LoPlatformMeshcore.h @@ -0,0 +1,35 @@ +#pragma once +#include "core/LoBBSArch.h" +#include + +struct LoBBSKernel; + +class BaseChatMesh; + +namespace mesh { +class RTCClock; +} + +#if defined(LOFS_NRF52) +class Adafruit_LittleFS; +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, Adafruit_LittleFS &filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock); +#elif LOBBS_ARCH_ESP32 +namespace fs { +class SPIFFSFS; +} +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, fs::SPIFFSFS &filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock); +#elif LOBBS_ARCH_RP2040 +class LittleFSFS; +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, LittleFSFS &filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock); +#else +void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, void *filesystem, const uint8_t *selfPubKey32, + mesh::RTCClock &rtcClock); +#endif + +void lobbsMeshCoreLoop(LoBBSKernel *core); +bool lobbsMeshCoreHandleDm(LoBBSKernel *core, const void *contact, const char *text); +bool lobbsMeshCoreDeliverMail(LoBBSKernel *core, const void *contact, const char *text); +bool lobbsMeshCoreHandleSerialLine(LoBBSKernel *core, const char *line); diff --git a/platforms/meshtastic/LoBBSDispatchMeshtastic.cpp b/platforms/meshtastic/LoBBSDispatchMeshtastic.cpp new file mode 100644 index 0000000..e8ef899 --- /dev/null +++ b/platforms/meshtastic/LoBBSDispatchMeshtastic.cpp @@ -0,0 +1,42 @@ +#if defined(LOBBS_PLATFORM_MESHTASTIC) +#include "platforms/meshtastic/LoBBSDispatchMeshtastic.h" +#include "core/LoBBSDispatch.h" +#include "core/LoBBSKernel.h" +#include "core/LoWire.h" +#include "mesh/MeshTypes.h" +#include + +LoInbound lobbsInboundFromPacket(const meshtastic_MeshPacket &mp, char *payloadBuf, size_t payloadBufLen) +{ + LoInbound in; + in.replyContext = ∓ + in.from.key = getFrom(&mp); + in.channel = (mp.from == 0) ? LoInboundChannel::Local : LoInboundChannel::Radio; + in.text = payloadBuf; + if (payloadBuf && payloadBufLen > 0 && mp.decoded.payload.size > 0) { + size_t copyLen = mp.decoded.payload.size; + if (copyLen >= payloadBufLen) + copyLen = payloadBufLen - 1; + memcpy(payloadBuf, mp.decoded.payload.bytes, copyLen); + payloadBuf[copyLen] = '\0'; + } else if (payloadBuf && payloadBufLen > 0) { + payloadBuf[0] = '\0'; + } + return in; +} + +ProcessMessage lobbsDispatchReceived(LoBBSKernel *kernel, const meshtastic_MeshPacket &mp) +{ + if (!isToUs(&mp)) + return ProcessMessage::CONTINUE; + + if (mp.decoded.payload.size == 0) + return ProcessMessage::CONTINUE; + + static char payloadBuf[256]; + LoInbound in = lobbsInboundFromPacket(mp, payloadBuf, sizeof(payloadBuf)); + + lobbsCoreHandleInbound(kernel, in); + return ProcessMessage::CONTINUE; +} +#endif diff --git a/platforms/meshtastic/LoBBSDispatchMeshtastic.h b/platforms/meshtastic/LoBBSDispatchMeshtastic.h new file mode 100644 index 0000000..1e3ea57 --- /dev/null +++ b/platforms/meshtastic/LoBBSDispatchMeshtastic.h @@ -0,0 +1,9 @@ +#pragma once +#include "core/LoWire.h" +#include "MeshModule.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +class LoBBSKernel; + +LoInbound lobbsInboundFromPacket(const meshtastic_MeshPacket &mp, char *payloadBuf, size_t payloadBufLen); +ProcessMessage lobbsDispatchReceived(LoBBSKernel *kernel, const meshtastic_MeshPacket &mp); diff --git a/platforms/meshtastic/LoBBSModule.cpp b/platforms/meshtastic/LoBBSModule.cpp new file mode 100644 index 0000000..8f334a3 --- /dev/null +++ b/platforms/meshtastic/LoBBSModule.cpp @@ -0,0 +1,14 @@ +#if defined(LOBBS_PLATFORM_MESHTASTIC) +#include "platforms/meshtastic/LoBBSModule.h" +#include "platforms/meshtastic/LoBBSDispatchMeshtastic.h" + +LoBBSModule::LoBBSModule() : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_MESSAGE_APP) +{ + kernel_.begin(); +} + +ProcessMessage LoBBSModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + return lobbsDispatchReceived(&kernel_, mp); +} +#endif diff --git a/platforms/meshtastic/LoBBSModule.h b/platforms/meshtastic/LoBBSModule.h new file mode 100644 index 0000000..88aee7e --- /dev/null +++ b/platforms/meshtastic/LoBBSModule.h @@ -0,0 +1,22 @@ +#pragma once + +#include "core/LoBBSKernel.h" +#include "MeshModule.h" +#include "SinglePortModule.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +class LoBBSModule : public SinglePortModule +{ + public: + LoBBSModule(); + ~LoBBSModule() override = default; + + LoBBSKernel &kernel() { return kernel_; } + const LoBBSKernel &kernel() const { return kernel_; } + + protected: + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + private: + LoBBSKernel kernel_; +}; diff --git a/platforms/meshtastic/LoPlatformMeshtastic.cpp b/platforms/meshtastic/LoPlatformMeshtastic.cpp new file mode 100644 index 0000000..5780eda --- /dev/null +++ b/platforms/meshtastic/LoPlatformMeshtastic.cpp @@ -0,0 +1,160 @@ +#if defined(LOBBS_PLATFORM_MESHTASTIC) +#include "core/LoBBSArch.h" +#include "platforms/LoPlatform.h" +#include "platforms/arduino/LoPlatformArduino.h" +#include "core/LoBBSReply.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "MeshService.h" +#include "mesh/MeshModule.h" +#include "mesh/NodeDB.h" +#include "mesh/Router.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include "mesh/generated/meshtastic/portnums.pb.h" +#include +#include +#include +#include + +#if LOBBS_ARCH_NRF52 && __has_include() +#include +#endif + +uint32_t lobbsPlatformUnixNow() +{ + return getTime(); +} + +LoPlatformSetUnixResult lobbsPlatformSetUnix(uint32_t unixSec) +{ + struct timeval tv; + tv.tv_sec = (time_t)unixSec; + tv.tv_usec = 0; + RTCSetResult r = perhapsSetRTC(RTCQualityNTP, &tv, true); + if (r == RTCSetResultSuccess) + return LoPlatformSetUnixResult::Ok; + if (r == RTCSetResultInvalidTime) + return LoPlatformSetUnixResult::Invalid; + return LoPlatformSetUnixResult::Failed; +} + +const char *lobbsPlatformClockName() +{ + return RtcName(getRTCQuality()); +} + +void lobbsPlatformLog(LoPlatformLogLevel level, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + char buf[256]; + lobbsPlatformLogFormat(buf, sizeof(buf), fmt, args); + va_end(args); + switch (level) { + case LoPlatformLogLevel::Debug: + LOG_DEBUG("%s", buf); + break; + case LoPlatformLogLevel::Info: + LOG_INFO("%s", buf); + break; + case LoPlatformLogLevel::Warn: + LOG_WARN("%s", buf); + break; + case LoPlatformLogLevel::Error: + LOG_ERROR("%s", buf); + break; + } +} + +void lobbsPlatformAfterFormat(const char *mount) +{ + if (!mount || strcmp(mount, "internal") != 0) + return; + if (nodeDB) + nodeDB->saveToDisk(); +} + +void lobbsPlatformNoteHandlerStack() +{ +#if LOBBS_ARCH_NRF52 && __has_include() + static bool lowStackWarned = false; + unsigned headroom = (unsigned)(uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t)); + if (!lowStackWarned && headroom < 1024) { + lowStackWarned = true; + lobbsPlatformLog(LoPlatformLogLevel::Warn, "LoBBS loop stack headroom low: %u B", headroom); + } +#endif +} + +uint32_t lobbsPlatformLocalNodeId() +{ + return nodeDB ? nodeDB->getNodeNum() : 0; +} + +size_t lobbsPlatformMaxReplyBytes() +{ + return LOBBS_REPLY_BYTES; +} + +static void lobbsSendMeshReply(const meshtastic_MeshPacket &req, const char *msg, size_t maxBytes) +{ + if (!msg || !service) + return; + if (!router) + return; + meshtastic_MeshPacket *reply = router->allocForSending(); + if (!reply) + return; + reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + + static constexpr char truncMarker[] = "[...]"; + static constexpr size_t truncMarkerLen = sizeof(truncMarker) - 1; + + size_t msgLen = strlen(msg); + bool isTruncated = msgLen > maxBytes; + size_t payloadSize = isTruncated ? maxBytes : msgLen; + reply->decoded.payload.size = payloadSize; + + if (isTruncated) { + size_t copyLen = maxBytes > truncMarkerLen ? maxBytes - truncMarkerLen : 0; + memcpy(reply->decoded.payload.bytes, msg, copyLen); + memcpy(reply->decoded.payload.bytes + copyLen, truncMarker, truncMarkerLen); + } else { + memcpy(reply->decoded.payload.bytes, msg, payloadSize); + } + setReplyTo(reply, req); + reply->decoded.want_response = false; + const bool ccPhone = (req.from == 0); + service->sendToMesh(reply, RX_SRC_LOCAL, ccPhone); +} + +void lobbsPlatformSendReply(const LoInbound &inbound, const char *msg) +{ + if (!inbound.replyContext) + return; + lobbsSendMeshReply(*static_cast(inbound.replyContext), msg, lobbsPlatformMaxReplyBytes()); +} + +bool lobbsPlatformInstallAuthorized(const LoInbound &inbound) +{ + if (inbound.channel == LoInboundChannel::Local) + return true; + if (!inbound.replyContext) + return false; + const meshtastic_MeshPacket &mp = *static_cast(inbound.replyContext); + if (mp.from == 0) + return true; +#if !MESHTASTIC_EXCLUDE_PKI + if (mp.pki_encrypted) { + if ((config.security.admin_key[0].size == 32 && + memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || + (config.security.admin_key[1].size == 32 && + memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || + (config.security.admin_key[2].size == 32 && + memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) + return true; + } +#endif + return false; +} +#endif diff --git a/platforms/meshtastic/extra_script.py b/platforms/meshtastic/extra_script.py deleted file mode 100644 index 343055a..0000000 --- a/platforms/meshtastic/extra_script.py +++ /dev/null @@ -1,3 +0,0 @@ -Import("env") - -env.Append(CPPDEFINES=[("LOBBS_PLATFORM_MESHTASTIC", "1")]) diff --git a/platforms/native/LoPlatformNative.cpp b/platforms/native/LoPlatformNative.cpp new file mode 100644 index 0000000..e0c2113 --- /dev/null +++ b/platforms/native/LoPlatformNative.cpp @@ -0,0 +1,82 @@ +#if defined(LOBBS_PLATFORM_NATIVE) +#include "platforms/LoPlatform.h" +#include "core/LoBBSReply.h" +#include +#include +#include +#include + +static std::chrono::steady_clock::time_point gStart = std::chrono::steady_clock::now(); + +uint32_t lobbsPlatformMillis() +{ + auto ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - gStart); + return (uint32_t)ms.count(); +} + +uint32_t lobbsPlatformUnixNow() +{ + return 1000; +} + +LoPlatformSetUnixResult lobbsPlatformSetUnix(uint32_t unixSec) +{ + (void)unixSec; + return LoPlatformSetUnixResult::Ok; +} + +const char *lobbsPlatformClockName() +{ + return "native"; +} + +uint32_t lobbsPlatformRandom(uint32_t bound) +{ + if (bound == 0) + return 0; + static uint32_t state = 1; + state = state * 1664525u + 1013904223u; + return state % bound; +} + +void lobbsPlatformLog(LoPlatformLogLevel level, const char *fmt, ...) +{ + (void)level; + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fputc('\n', stderr); + va_end(args); +} + +void lobbsPlatformAfterFormat(const char *mount) +{ + (void)mount; +} + +void lobbsPlatformNoteHandlerStack() +{ +} + +uint32_t lobbsPlatformLocalNodeId() +{ + return 0xBB500001; +} + +size_t lobbsPlatformMaxReplyBytes() +{ + return LOBBS_REPLY_BYTES; +} + +void lobbsPlatformSendReply(const LoInbound &inbound, const char *msg) +{ + (void)inbound; + (void)msg; +} + +bool lobbsPlatformInstallAuthorized(const LoInbound &inbound) +{ + return inbound.channel == LoInboundChannel::Local; +} + +#endif diff --git a/protocol/machine/paginate.cpp b/protocol/machine/paginate.cpp index 32cdb3b..8228ecc 100644 --- a/protocol/machine/paginate.cpp +++ b/protocol/machine/paginate.cpp @@ -1,8 +1,9 @@ #include "paginate.h" -#include "LoBBSReply.h" +#include "core/LoBBSReply.h" +#include "platforms/LoPlatform.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" bool lobbsPaginateMachine(uint32_t reqId, const std::string &document, uint32_t page1, std::string &pageOut, const char **errMsg) { @@ -12,7 +13,8 @@ bool lobbsPaginateMachine(uint32_t reqId, const std::string &document, uint32_t char prefix[40]; size_t prefixLen = (size_t)snprintf(prefix, sizeof(prefix), "<%u>ok\n", reqId); - if (prefixLen + document.size() <= LOBBS_REPLY_BYTES) { + const size_t pageBytes = lobbsPlatformMaxReplyBytes(); + if (prefixLen + document.size() <= pageBytes) { if (page1 > 1) { if (errMsg) *errMsg = "No such page."; @@ -29,7 +31,7 @@ bool lobbsPaginateMachine(uint32_t reqId, const std::string &document, uint32_t uint32_t pages = 0; while (true) { prefixLen = (size_t)snprintf(prefix, sizeof(prefix), "<%u>ok [%u:%u]\n", reqId, widest, widest); - budget = LOBBS_REPLY_BYTES - prefixLen; + budget = pageBytes - prefixLen; pages = (uint32_t)((document.size() + budget - 1) / budget); if (pages <= widest) break; diff --git a/protocol/machine/serialize.cpp b/protocol/machine/serialize.cpp index 3cbe278..6e19dca 100644 --- a/protocol/machine/serialize.cpp +++ b/protocol/machine/serialize.cpp @@ -1,7 +1,7 @@ #include "serialize.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" bool lobbsSerializeMachine(const LoBBSResponse &resp, std::string &out) { diff --git a/protocol/machine/serialize.h b/protocol/machine/serialize.h index ffb536a..3d86066 100644 --- a/protocol/machine/serialize.h +++ b/protocol/machine/serialize.h @@ -1,5 +1,5 @@ #pragma once -#include "LoBBSResponse.h" +#include "core/LoBBSResponse.h" #include bool lobbsSerializeMachine(const LoBBSResponse &resp, std::string &out); diff --git a/protocol/plain_text/paginate.cpp b/protocol/plain_text/paginate.cpp index 5b1cd6f..c04028e 100644 --- a/protocol/plain_text/paginate.cpp +++ b/protocol/plain_text/paginate.cpp @@ -1,10 +1,11 @@ #include "paginate.h" -#include "LoBBSReply.h" +#include "core/LoBBSReply.h" +#include "platforms/LoPlatform.h" #include #include #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static constexpr size_t LOBBS_PAGER_FOOTER_MAX = 16; static constexpr int LOBBS_PAGER_MAX_PAGES = 64; @@ -95,14 +96,15 @@ bool lobbsPaginatePlainText(const std::string &text, uint32_t page1, std::string uint32_t pageStarts[LOBBS_PAGER_MAX_PAGES + 1]; memset(pageStarts, 0, sizeof(pageStarts)); int pageCount = 0; - lobbsHumanPackPages(lines, LOBBS_REPLY_BYTES, true, pageStarts, pageCount); + const size_t pageBytes = lobbsPlatformMaxReplyBytes(); + lobbsHumanPackPages(lines, pageBytes, true, pageStarts, pageCount); if (pageCount <= 0) { if (errMsg) *errMsg = "Empty."; return false; } if (pageCount == 1) { - lobbsHumanPackPages(lines, LOBBS_REPLY_BYTES, false, pageStarts, pageCount); + lobbsHumanPackPages(lines, pageBytes, false, pageStarts, pageCount); pageStarts[1] = (uint32_t)lines.size(); } if (page1 > (uint32_t)pageCount) { @@ -114,7 +116,7 @@ bool lobbsPaginatePlainText(const std::string &text, uint32_t page1, std::string uint32_t from = pageStarts[page1 - 1]; uint32_t to = pageStarts[page1]; size_t footerReserve = pageCount > 1 ? LOBBS_PAGER_FOOTER_MAX : 0; - size_t maxLine = LOBBS_REPLY_BYTES > footerReserve ? LOBBS_REPLY_BYTES - footerReserve : 0; + size_t maxLine = pageBytes > footerReserve ? pageBytes - footerReserve : 0; for (uint32_t i = from; i < to; i++) { if (i > from) pageOut.push_back('\n'); diff --git a/protocol/plain_text/serialize.cpp b/protocol/plain_text/serialize.cpp index 9a2de94..0263d91 100644 --- a/protocol/plain_text/serialize.cpp +++ b/protocol/plain_text/serialize.cpp @@ -1,8 +1,8 @@ #include "serialize.h" -#include "LoBBSHooks.h" +#include "core/LoBBSHooks.h" #include -#include "LoBBSStackGuard.h" +#include "core/LoBBSStackGuard.h" static void lobbsDisplayGeneric(const LoScalar &record, std::string &lineOut) { diff --git a/protocol/plain_text/serialize.h b/protocol/plain_text/serialize.h index 5f86c43..a6b198e 100644 --- a/protocol/plain_text/serialize.h +++ b/protocol/plain_text/serialize.h @@ -1,6 +1,6 @@ #pragma once -#include "LoBBSCommandCtx.h" -#include "LoBBSResponse.h" +#include "core/LoBBSCommandCtx.h" +#include "core/LoBBSResponse.h" #include bool lobbsSerializePlainText(const LoBBSCommandCtx &ctx, const LoBBSResponse &resp, std::string &out); diff --git a/tests/command_tests.h b/tests/command_tests.h index e35f8a8..fffb7a4 100644 --- a/tests/command_tests.h +++ b/tests/command_tests.h @@ -1,47 +1,31 @@ #pragma once -#include "LoBBSConfig.h" +#include "core/LoBBSConfig.h" #if LOBBS_SEED -#include "LoBBSDispatch.h" -#include "LoBBSInstall.h" -#include "LoBBSModule.h" -#include "LoBBSReplyCache.h" -#include "LoBBSSeed.h" -#include "gps/RTC.h" +#include "core/LoBBSInstall.h" +#include "core/LoBBSKernel.h" +#include "core/LoBBSReplyCache.h" +#include "core/LoBBSSeed.h" #include "lofs/LoFS.h" -#include "mesh/NodeDB.h" -#include +#include "tests/meshtastic_fixture.h" +#if LOBBS_ARCH_PORTDUINO +#include "FSCommon.h" +#include "PortduinoFS.h" +#include +#include +#include +#endif #include #include #include #include -static constexpr uint32_t kLobbsTestBbsNode = 0xBB500001; -static constexpr uint32_t kLobbsTestClientNode = 0xABCD0001; - -class LobbsTestNodeDB : public NodeDB -{ - public: - LobbsTestNodeDB() { myNodeInfo.my_node_num = kLobbsTestBbsNode; } -}; +extern std::vector *lobbsTestReplySink; static std::unique_ptr lobbsTestNodeDb; -static std::unique_ptr lobbsTestModule; +static std::unique_ptr lobbsTestModule; static std::vector lobbsTestReplies; -static void lobbsTestSendLine(const char *line) -{ - meshtastic_MeshPacket mp = {}; - mp.from = kLobbsTestClientNode; - mp.to = kLobbsTestBbsNode; - size_t len = strlen(line); - if (len > sizeof(mp.decoded.payload.bytes)) - len = sizeof(mp.decoded.payload.bytes); - memcpy(mp.decoded.payload.bytes, line, len); - mp.decoded.payload.size = len; - lobbsDispatchReceived(lobbsTestModule.get(), mp); -} - static const char *lobbsTestLastReply() { if (lobbsTestReplies.empty()) @@ -49,35 +33,49 @@ static const char *lobbsTestLastReply() return lobbsTestReplies.back().c_str(); } +#if LOBBS_ARCH_PORTDUINO +static void lobbsTestEnsurePortduinoFs() +{ + static char fsRoot[256]; + snprintf(fsRoot, sizeof(fsRoot), "/tmp/lobbs-cmdtest-%d", (int)getpid()); + mkdir(fsRoot, 0755); + portduinoVFS->mountpoint(fsRoot); +} +#endif + static void lobbsTestFixtureSetUp() { - resetRTCStateForTests(); - struct timeval tv = {1000, 0}; - setRTCSystemTimeForTests(&tv); - lobbsTestNodeDb = std::make_unique(); - nodeDB = lobbsTestNodeDb.get(); + lobbsTestResetRtc(); +#if LOBBS_ARCH_PORTDUINO + lobbsTestEnsurePortduinoFs(); +#endif + lobbsTestBindNodeDb(lobbsTestNodeDb); lobbsTestReplies.clear(); lobbsTestReplySink = &lobbsTestReplies; - lobbsTestModule = std::make_unique(); +#if LOBBS_ARCH_PORTDUINO + fsInit(); +#endif + LoFS::resetForTests(); + lobbsTestModule = std::make_unique(); + lobbsTestModule->begin(); +#if LOBBS_ARCH_PORTDUINO + LoFS::rmdir("/internal/lobbs-test-dir", true); + LoFS::rmdir("/internal/tmp", true); +#endif lobbsInstallAutoSeed(*lobbsTestModule); lobbsSeedAll(*lobbsTestModule); -} - -static void lobbsTestSendLocalLine(const char *line) -{ - meshtastic_MeshPacket mp = {}; - mp.from = 0; - mp.to = kLobbsTestBbsNode; - size_t len = strlen(line); - memcpy(mp.decoded.payload.bytes, line, len); - mp.decoded.payload.size = len; - lobbsDispatchReceived(lobbsTestModule.get(), mp); + if (lobbsTestModule->installState() != LoBBSInstallState::Ready) { + TEST_FAIL_MESSAGE("lobbsInstallAutoSeed did not reach Ready"); + return; + } } static void lobbsTestReboot() { lobbsTestModule.reset(); - lobbsTestModule = std::make_unique(); + LoFS::resetForTests(); + lobbsTestModule = std::make_unique(); + lobbsTestModule->begin(); } static void lobbsTestFixtureTearDown() @@ -85,302 +83,297 @@ static void lobbsTestFixtureTearDown() lobbsTestReplySink = nullptr; lobbsTestReplies.clear(); lobbsTestModule.reset(); - lobbsTestNodeDb.reset(); - nodeDB = nullptr; - resetRTCStateForTests(); + lobbsTestClearNodeDb(lobbsTestNodeDb); } static void test_command_mail_list_and_plain_page2() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/mail list"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail list"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "{p 1/")); - lobbsTestSendLine("/p2"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/p2"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "{p 2/")); } static void test_command_machine_page_from_cache() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/42 mail list"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/42 mail list"); const char *p1 = lobbsTestLastReply(); TEST_ASSERT_NOT_NULL(strstr(p1, "<42>ok [1:")); - lobbsTestSendLine("/43 p2"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/43 p2"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "<43>ok [2:")); } static void test_command_no_such_page() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/mail list"); - lobbsTestSendLine("/p99"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail list"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/p99"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No such page.")); } static void test_command_new_command_replaces_cache() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/mail list"); - lobbsTestSendLine("/time"); - lobbsTestSendLine("/p2"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail list"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/time"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/p2"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No such page.")); } static void test_command_error_keeps_cache() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/mail list"); - lobbsTestSendLine("/mail read 99999"); - lobbsTestSendLine("/p2"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail list"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail read 99999"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/p2"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "{p 2/")); } static void test_command_cache_expires() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/mail list"); - struct timeval tv = {1000 + LOBBS_REPLY_CACHE_TTL_SEC + 1, 0}; - setRTCSystemTimeForTests(&tv); - lobbsTestSendLine("/p2"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail list"); + setBootRelativeTimeForUnitTest(1000 + LOBBS_REPLY_CACHE_TTL_SEC + 1); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/p2"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No cached reply.")); } static void test_command_fs_cwd() { - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/pwd"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/pwd"); TEST_ASSERT_EQUAL_STRING("/", lobbsTestLastReply()); - lobbsTestSendLine("/cd /no/such/dir"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/cd /no/such/dir"); TEST_ASSERT_EQUAL_STRING("No such directory.", lobbsTestLastReply()); - lobbsTestSendLine("/rm relative.txt"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm relative.txt"); TEST_ASSERT_EQUAL_STRING("Absolute path only.", lobbsTestLastReply()); - lobbsTestSendLine("/rmtree /flash/.. /flash/.."); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rmtree /internal/.. /internal/.."); TEST_ASSERT_EQUAL_STRING("Refused.", lobbsTestLastReply()); - lobbsTestSendLine("/cd /flash"); - lobbsTestSendLine("/cd .."); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/cd /internal"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/cd .."); TEST_ASSERT_EQUAL_STRING("/", lobbsTestLastReply()); } static void test_command_fs_mounts_and_tools() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/ls /"); - TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "flash")); - lobbsTestSendLine("/mkdir /flash/lobbs-test-dir"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/ls /"); + TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "internal")); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mkdir /internal/lobbs-test-dir"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Created.")); - lobbsTestSendLine("/stat /flash/lobbs-test-dir"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/stat /internal/lobbs-test-dir"); TEST_ASSERT_EQUAL_STRING("dir", lobbsTestLastReply()); - lobbsTestSendLine("/mkdir /flash/lobbs-test-dir"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mkdir /internal/lobbs-test-dir"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Already exists.")); - lobbsTestSendLine("/df"); - TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "flash: ")); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/df"); + TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "internal: ")); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "[lobbs]")); - lobbsTestSendLine("/ls /flash/lobbs"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/ls /internal/lobbs"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "db/")); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "install.ls ")); - lobbsTestSendLine("/rmdir /flash/lobbs-test-dir"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rmdir /internal/lobbs-test-dir"); } static void test_command_fs_format_guards() { - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/format flash"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/format internal"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "This uninstalls LoBBS.")); - lobbsTestSendLine("/format flash 000000"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/format internal 000000"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Wrong or expired code.")); - lobbsTestSendLine("/format nope"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/format nope"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No such mount.")); } static void test_command_fs_cp_mv() { - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/cp /flash/lobbs/install.ls /flash/lobbs-copy.ls"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/cp /internal/lobbs/install.ls /internal/lobbs-copy.ls"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Copied.")); - lobbsTestSendLine("/cp /flash/lobbs/install.ls /flash/lobbs-copy.ls"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/cp /internal/lobbs/install.ls /internal/lobbs-copy.ls"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Destination exists.")); - lobbsTestSendLine("/mv /flash/lobbs-copy.ls /flash/lobbs-moved.ls"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mv /internal/lobbs-copy.ls /internal/lobbs-moved.ls"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Moved.")); - lobbsTestSendLine("/rm /flash/lobbs-moved.ls"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/lobbs-moved.ls"); } static void test_command_fs_upload_commit() { - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/mkdir /flash/tmp"); - lobbsTestSendLine("/rm /flash/tmp/upload-a.bin"); - lobbsTestSendLine("/rm /flash/tmp/upload-b.bin"); - lobbsTestSendLine("/rm /flash/files/upload-done.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mkdir /internal/tmp"); + TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Created.")); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/upload-a.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/upload-b.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/files/upload-done.bin"); - lobbsTestSendLine("/upload /flash/tmp/upload-a.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/upload-a.bin"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Size 0.")); - lobbsTestSendLine("/upload /flash/tmp/upload-a.bin 10:AAAA"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/upload-a.bin 10:AAAA"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Gap:")); - lobbsTestSendLine("/upload /flash/tmp/upload-a.bin 0:8xhIpNzLldv25utFy"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/upload-a.bin 0:8xhIpNzLldv25utFy"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Size 12.")); - lobbsTestSendLine("/upload /flash/tmp/upload-a.bin 0:8xhIpNzLldv25utFy"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/upload-a.bin 0:8xhIpNzLldv25utFy"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Size 12.")); - lobbsTestSendLine("/commit /flash/tmp/upload-a.bin /flash/files/upload-done.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/commit /internal/tmp/upload-a.bin /internal/files/upload-done.bin"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No CRC in name.")); - lobbsTestSendLine("/upload /flash/tmp/upload-b.bin.01020304.tmp 0:8xhIpNzLldv25utFy"); - lobbsTestSendLine("/commit /flash/tmp/upload-b.bin.01020304.tmp /flash/files/upload-done.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/upload-b.bin.01020304.tmp 0:8xhIpNzLldv25utFy"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/commit /internal/tmp/upload-b.bin.01020304.tmp /internal/files/upload-done.bin"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "CRC mismatch.")); - lobbsTestSendLine("/rm /flash/tmp/upload-a.bin"); - lobbsTestSendLine("/rm /flash/tmp/upload-b.bin.01020304.tmp"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/upload-a.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/upload-b.bin.01020304.tmp"); } /** Two sequential chunks: offset 0 then offset == size (append). Payload is "hello world\\n". */ static void test_command_fs_upload_append_two_chunks() { - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/mkdir /flash/tmp"); - lobbsTestSendLine("/rm /flash/tmp/append-two.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mkdir /internal/tmp"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/append-two.bin"); - lobbsTestSendLine("/upload /flash/tmp/append-two.bin 0:0Waqlj8GO"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/append-two.bin 0:0Waqlj8GO"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Size 6.")); - lobbsTestSendLine("/upload /flash/tmp/append-two.bin 6:0bHyFj1oY"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/append-two.bin 7:0bHyFj1oY"); + TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Gap:")); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/append-two.bin 6:0bHyFj1oY"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Size 12.")); - lobbsTestSendLine("/upload /flash/tmp/append-two.bin 7:0bHyFj1oY"); - TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Gap:")); - - lobbsTestSendLine("/rm /flash/tmp/append-two.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/append-two.bin"); } static void test_command_fs_upload_commit_ok() { - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/mkdir /flash/tmp"); - lobbsTestSendLine("/mkdir /flash/files"); - lobbsTestSendLine("/rm /flash/tmp/chunk.ok.af083b2d.tmp"); - lobbsTestSendLine("/rm /flash/files/chunk.ok.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mkdir /internal/tmp"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mkdir /internal/files"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/chunk.ok.af083b2d.tmp"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/files/chunk.ok.bin"); - lobbsTestSendLine("/upload /flash/tmp/chunk.ok.af083b2d.tmp 0:8xhIpNzLldv25utFy"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/upload /internal/tmp/chunk.ok.af083b2d.tmp 0:8xhIpNzLldv25utFy"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Size 12.")); - lobbsTestSendLine("/commit /flash/tmp/chunk.ok.af083b2d.tmp /flash/files/chunk.ok.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/commit /internal/tmp/chunk.ok.af083b2d.tmp /internal/files/chunk.ok.bin"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Moved.")); - lobbsTestSendLine("/cat /flash/files/chunk.ok.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/cat /internal/files/chunk.ok.bin"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "hello world")); - lobbsTestSendLine("/rm /flash/files/chunk.ok.bin"); - lobbsTestSendLine("/rm /flash/tmp/chunk.ok.af083b2d.tmp"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/files/chunk.ok.bin"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/rm /internal/tmp/chunk.ok.af083b2d.tmp"); } static void test_command_install_guards() { - lobbsTestSendLine("/install flash hacker pass"); - TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Not authorized.")); - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/install flash other pass"); - TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Already installed at /flash.")); - lobbsTestSendLine("/login demo99 demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/install internal other pass"); + TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Already installed at /internal.")); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login demo99 demo1"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Welcome demo99")); } static void test_command_install_autodetect() { lobbsTestReboot(); - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Welcome")); } static void test_command_install_blank_and_local() { - LoFS::rmdir("/flash/lobbs", true); + LoFS::rmdir("/internal/lobbs", true); lobbsTestReboot(); - lobbsTestSendLine("/whoami"); - TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "/install ")); - lobbsTestSendLine("/install flash boss pw"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/whoami"); + TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "/install ")); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/install internal boss pw"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Not authorized.")); - lobbsTestSendLocalLine("/install sd boss pw"); + lobbsTestSendLocalLine(lobbsTestModule.get(), "/install sd boss pw"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Mount not available.")); - lobbsTestSendLocalLine("/install flash boss pw"); + lobbsTestSendLocalLine(lobbsTestModule.get(), "/install internal boss pw"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Installed.")); - lobbsTestSendLine("/login newbie pw"); - lobbsTestSendLine("/df"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login newbie pw"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/df"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "SysOp only.")); } static void test_command_install_adopts_existing() { - LoFS::remove("/flash/lobbs/install.ls"); + LoFS::remove("/internal/lobbs/install.ls"); lobbsTestReboot(); - lobbsTestSendLocalLine("/install flash demo01 demo1"); + lobbsTestSendLocalLine(lobbsTestModule.get(), "/install internal demo01 demo1"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "sysop credentials required")); - lobbsTestSendLocalLine("/install flash sysop demo1"); + lobbsTestSendLocalLine(lobbsTestModule.get(), "/install internal sysop demo1"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Installed.")); } static void test_command_users_kick() { - lobbsTestSendLine("/login demo03 demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login demo03 demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/users kick demo03"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/users kick demo03"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Sessions cleared.")); } static void test_command_config() { - lobbsTestSendLine("/login demo01 demo1"); - lobbsTestSendLine("/config"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login demo01 demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "SysOp only.")); - lobbsTestSendLine("/login sysop demo1"); - lobbsTestSendLine("/config"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Use /help config key for details")); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "session.max 16")); - lobbsTestSendLine("/help config session.max"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/help config session.max"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "session.max: Max concurrent login sessions. Default 16, range 1-64.")); - lobbsTestSendLine("/help config bogus"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/help config bogus"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No help found for config bogus")); - lobbsTestSendLine("/config session.max"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config session.max"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "session.max = 16")); - lobbsTestSendLine("/config session.max 0"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config session.max 0"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Must be 1-64.")); - lobbsTestSendLine("/config session.max 8"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config session.max 8"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "session.max = 8.")); - lobbsTestSendLine("/config session.max reset"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config session.max reset"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "session.max reset.")); - lobbsTestSendLine("/config bogus"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config bogus"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Unknown setting.")); - lobbsTestSendLine("/config password.min 8"); - lobbsTestSendLine("/passwd short short"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/config password.min 8"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/passwd short short"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Password too short.")); - lobbsTestSendLine("/passwd longenuf longenuf"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/passwd longenuf longenuf"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Password updated.")); } static void test_command_subcommand_with_args() { - lobbsTestSendLine("/login sysop demo1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/login sysop demo1"); lobbsTestReplies.clear(); - lobbsTestSendLine("/news read 1"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/news read 1"); TEST_ASSERT_NULL(strstr(lobbsTestLastReply(), "Unknown command")); - lobbsTestSendLine("/mail send demo01 hello there"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/mail send demo01 hello there"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "Mail sent.")); } static void test_command_help_catalog_and_topics() { - lobbsTestSendLine("/help"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/help"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), " Help\n")); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "pN (page N of the last reply)")); - lobbsTestSendLine("/help bogus"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/help bogus"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "No help found for bogus")); - lobbsTestSendLine("/help news read"); + lobbsTestSendRadioLine(lobbsTestModule.get(),"/help news read"); TEST_ASSERT_NOT_NULL(strstr(lobbsTestLastReply(), "read N")); TEST_ASSERT_NULL(strstr(lobbsTestLastReply(), "post message")); } diff --git a/tests/meshtastic_fixture.h b/tests/meshtastic_fixture.h new file mode 100644 index 0000000..e0eca36 --- /dev/null +++ b/tests/meshtastic_fixture.h @@ -0,0 +1,72 @@ +#pragma once +#if LOBBS_SEED + +#include "core/LoBBSDispatch.h" +#include "core/LoWire.h" +#include "gps/RTC.h" +#include "mesh/NodeDB.h" +#include +#include + +static constexpr uint32_t kLobbsTestBbsNode = 0xBB500001; +static constexpr uint32_t kLobbsTestClientNode = 0xABCD0001; + +class LobbsTestNodeDB : public NodeDB +{ + public: + LobbsTestNodeDB() { myNodeInfo.my_node_num = kLobbsTestBbsNode; } +}; + +inline void lobbsTestResetRtc() +{ + resetRTCStateForTests(); + struct timeval tv = {1000, 0}; + setRTCSystemTimeForTests(&tv); +} + +inline void lobbsTestBindNodeDb(std::unique_ptr &nodeDb) +{ + nodeDb = std::make_unique(); + nodeDB = nodeDb.get(); +} + +inline void lobbsTestClearNodeDb(std::unique_ptr &nodeDb) +{ + nodeDb.reset(); + nodeDB = nullptr; + resetRTCStateForTests(); +} + +inline void lobbsTestSendRadioLine(LoBBSKernel *kernel, const char *line, uint32_t fromNode = kLobbsTestClientNode) +{ + static char payloadBuf[256]; + LoInbound in; + in.from.key = fromNode; + in.channel = LoInboundChannel::Radio; + in.replyContext = nullptr; + size_t len = strlen(line); + if (len >= sizeof(payloadBuf)) + len = sizeof(payloadBuf) - 1; + memcpy(payloadBuf, line, len); + payloadBuf[len] = '\0'; + in.text = payloadBuf; + lobbsCoreHandleInbound(kernel, in); +} + +inline void lobbsTestSendLocalLine(LoBBSKernel *kernel, const char *line) +{ + static char payloadBuf[256]; + LoInbound in; + in.from.key = 0; + in.channel = LoInboundChannel::Local; + in.replyContext = nullptr; + size_t len = strlen(line); + if (len >= sizeof(payloadBuf)) + len = sizeof(payloadBuf) - 1; + memcpy(payloadBuf, line, len); + payloadBuf[len] = '\0'; + in.text = payloadBuf; + lobbsCoreHandleInbound(kernel, in); +} + +#endif diff --git a/tests/protocol_tests.h b/tests/protocol_tests.h index 0cd991d..d9a9960 100644 --- a/tests/protocol_tests.h +++ b/tests/protocol_tests.h @@ -1,6 +1,6 @@ #pragma once -#include "LoBBSReply.h" -#include "LoBBSResponse.h" +#include "core/LoBBSReply.h" +#include "core/LoBBSResponse.h" #include "protocol/machine/paginate.h" #include "protocol/machine/serialize.h" #include "protocol/plain_text/paginate.h" diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..196662b --- /dev/null +++ b/version.properties @@ -0,0 +1,4 @@ +[LOBBS] +major=2 +minor=0 +build=0