Files
pyxis/lib/tdeck_ui/Hardware/TDeck/SDArchiveFileSystem.h
T
torlando-agent[bot]andClaude Opus 4.8 70d4aa6be9 feat: graft pyxis onto upstream microReticulum 0.4.1
Repins microReticulum + microLXMF onto the upstream-0.4.1 graft and adapts
pyxis to the new src/microReticulum/ layout and 0.4.x APIs. The far-diverged
0.3.0 fork's Resource/Transport/Identity work is subsumed by upstream's
reimplementation; only the still-needed fixes ride on the pinned branches
(PKCS7/HMAC/X25519 crypto -- proven byte-identical to python RNS 1.3.1 --
Packet link-proof callback, Identity short-sig guard, and the bz2 layer +
decompress-on-receive in Resource::assemble()).

Consumer-side changes:
- platformio.ini: pin microReticulum @2f21fee (pyxis-fixes-on-0.4.1) and
  microLXMF @33760d0 (chore/microreticulum-0.4.1-layout); bump microStore
  ceea8f5 -> c5fb69d (0.4.x requires the new BasicFileStore::init API);
  -std=gnu++11 -> gnu++17 (upstream requires C++17).
- Namespace all microReticulum includes (angle + quote) to <microReticulum/...>
  for the relocated layout; shim-local Utilities/Stream.h|Print.h preserved.
- Interface::send_outgoing now returns bool: update TCP/BLE/SX1262/Auto
  overrides with correct success/failure returns.
- SDArchiveFileSystem::init(bool reformatOnFail=true) to match new microStore.
- Static Transport::get_path_table() -> path_table(); instance getter unchanged.
- Remove duplicate shim Cryptography/BZ2 (microReticulum provides it now; keep
  lib/libbz2 as the ESP32 bzlib provider).
- patch_littlefs_paths.py: normalize microStore's LittleFS adapter paths to a
  leading "/" -- ESP32 Arduino LittleFS rejects "./"-prefixed paths, which
  silently broke the path store (no peer paths learned, all messaging blocked).

Validated on T-Deck Plus: builds (RAM 27.5% / Flash 77.7%), boots stable
(no WDT/panic), and a full on-device LXMF e2e (DIRECT + OPPORTUNISTIC +
bz2-compressed-Resource receive) passes 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:49:44 -04:00

265 lines
9.4 KiB
C++

// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#ifndef HARDWARE_TDECK_SD_ARCHIVE_FS_H
#define HARDWARE_TDECK_SD_ARCHIVE_FS_H
#ifdef ARDUINO
#include "SDAccess.h"
#include <microStore/File.h>
#include <microStore/FileSystem.h>
#include <Arduino.h>
#include <SD.h>
#include <FS.h>
namespace Hardware {
namespace TDeck {
// microStore::FileSystem adapter that piggybacks on the SD card already
// mounted by SDAccess::init() and serializes every operation through
// the shared SPI bus mutex.
//
// The stock microStore SDFileSystem adapter calls SD.begin() in its own
// init() and uses its own SPIClass instance — that conflicts with the
// pyxis HW init order (display + LoRa share HSPI). This adapter skips
// re-mounting and just delegates to the global SD object, wrapping each
// op with SDAccess::acquire_bus() / release_bus() so it cooperates with
// display and LoRa traffic.
//
// Used as the archive tier for LXMF::MessageStore — the LittleFS hot
// tier holds the most-recent ~50 messages per conversation; everything
// older is moved here.
namespace _SDArchive {
class FileImpl : public microStore::FileImpl {
private:
fs::File _file;
bool _open;
public:
FileImpl(fs::File f) : microStore::FileImpl(), _file(f), _open(true) {}
virtual ~FileImpl() { if (_open) close(); }
inline virtual const char* name() const { return _file.name(); }
inline virtual size_t size() const { return _file.size(); }
inline virtual void close() {
if (!_open) return;
// Only release the bus if we actually took it — otherwise the
// matching xSemaphoreGive in release_bus() would be unmatched
// and skew the mutex counter. _file.close() under SPI contention
// is the lesser evil vs corrupting the bus mutex.
bool held = SDAccess::acquire_bus(500);
_file.close();
if (held) SDAccess::release_bus();
_open = false;
}
inline virtual int read() {
if (!SDAccess::acquire_bus(500)) return -1;
int r = _file.read();
SDAccess::release_bus();
return r;
}
inline virtual size_t read(uint8_t* buf, size_t sz) {
if (!SDAccess::acquire_bus(500)) return 0;
size_t r = _file.read(buf, sz);
SDAccess::release_bus();
return r;
}
inline virtual size_t write(uint8_t b) {
if (!SDAccess::acquire_bus(500)) return 0;
size_t w = _file.write(b);
SDAccess::release_bus();
return w;
}
inline virtual size_t write(const uint8_t* buf, size_t sz) {
if (!SDAccess::acquire_bus(500)) return 0;
size_t w = _file.write(buf, sz);
SDAccess::release_bus();
return w;
}
inline virtual int available() {
if (!SDAccess::acquire_bus(500)) return 0;
int a = _file.available();
SDAccess::release_bus();
return a;
}
inline virtual int peek() {
if (!SDAccess::acquire_bus(500)) return -1;
int p = _file.peek();
SDAccess::release_bus();
return p;
}
inline virtual size_t tell() {
if (!SDAccess::acquire_bus(500)) return 0;
size_t t = _file.position();
SDAccess::release_bus();
return t;
}
inline virtual long seek(uint32_t pos, microStore::SeekMode mode) {
if (!SDAccess::acquire_bus(500)) return -1;
fs::SeekMode smode = fs::SeekSet;
switch (mode) {
case microStore::SeekMode::SeekModeCur: smode = fs::SeekCur; break;
case microStore::SeekMode::SeekModeEnd: smode = fs::SeekEnd; break;
default: smode = fs::SeekSet; break;
}
long ok = _file.seek(pos, smode);
SDAccess::release_bus();
return ok;
}
inline virtual void flush() {
if (!SDAccess::acquire_bus(500)) return;
_file.flush();
SDAccess::release_bus();
}
inline virtual bool isValid() const { return _open && _file; }
};
class FileSystemImpl : public microStore::FileSystemImpl {
public:
FileSystemImpl() : microStore::FileSystemImpl() {}
virtual ~FileSystemImpl() {}
// SD is already mounted by SDAccess::init() — we don't re-mount.
virtual bool init(bool reformatOnFail = true) override { return SDAccess::is_ready(); }
virtual bool format() override { return false; }
virtual microStore::File open(const char* path, microStore::File::Mode mode,
const bool create = false) override {
const char* pmode = nullptr;
switch (mode) {
case microStore::File::ModeRead: pmode = FILE_READ; break;
case microStore::File::ModeWrite: pmode = FILE_WRITE; break;
case microStore::File::ModeAppend: pmode = FILE_APPEND; break;
case microStore::File::ModeReadWrite: pmode = "w+"; break;
case microStore::File::ModeReadAppend: pmode = "a+"; break;
default: return {};
}
if (!SDAccess::acquire_bus(500)) return {};
fs::File f = SD.open(path, pmode);
SDAccess::release_bus();
if (!f) return {};
return microStore::File(new FileImpl(f));
}
virtual bool exists(const char* path) override {
if (!SDAccess::acquire_bus(500)) return false;
bool r = SD.exists(path);
SDAccess::release_bus();
return r;
}
virtual bool remove(const char* path) override {
if (!SDAccess::acquire_bus(500)) return false;
bool r = SD.remove(path);
SDAccess::release_bus();
return r;
}
virtual bool rename(const char* from, const char* to) override {
if (!SDAccess::acquire_bus(500)) return false;
bool r = SD.rename(from, to);
SDAccess::release_bus();
return r;
}
virtual bool mkdir(const char* path) override {
if (!SDAccess::acquire_bus(500)) return false;
bool r = SD.mkdir(path);
SDAccess::release_bus();
return r;
}
virtual bool rmdir(const char* path) override {
if (!SDAccess::acquire_bus(500)) return false;
bool r = SD.rmdir(path);
SDAccess::release_bus();
return r;
}
virtual bool isDirectory(const char* path) override {
if (!SDAccess::acquire_bus(500)) return false;
fs::File f = SD.open(path, FILE_READ);
bool r = false;
if (f) {
r = f.isDirectory();
f.close();
}
SDAccess::release_bus();
return r;
}
virtual std::list<std::string> listDirectory(const char* path,
Callbacks::DirectoryListing callback = nullptr) override {
// Release the SPI bus mutex between directory entries — an archive
// with thousands of entries would otherwise hold the bus for
// multiple seconds, blocking LoRa TX/RX and display flush past
// their 500 ms acquire_bus timeout (LoRa TX fails silently,
// display tears; under inbound flood the RX FIFO could overflow).
// SDLib's File cursor lives in the `root` File object, and SD
// commands are per-block addressed, so other-CS bus users
// (display, LoRa) between our openNextFile calls don't clobber
// SD state. The callback is invoked outside the critical section
// so it can do work of its own without blocking other bus users.
std::list<std::string> files;
if (!SDAccess::acquire_bus(500)) return files;
fs::File root = SD.open(path);
if (!root) {
SDAccess::release_bus();
return files;
}
while (true) {
fs::File f = root.openNextFile();
if (!f) break;
bool is_dir = f.isDirectory();
std::string name;
if (!is_dir) name = f.name();
f.close();
// Drop the bus so other tasks can run between entries.
SDAccess::release_bus();
if (!is_dir) {
if (callback) callback(name.c_str());
else files.push_back(name);
}
// Re-acquire to advance the cursor / read the next entry.
// If we can't re-acquire within the timeout, stop early
// rather than spin — caller gets whatever we collected.
if (!SDAccess::acquire_bus(500)) {
// root is still open; ensure it gets closed. We can't
// call f.close() on the directory without the bus, so
// skip the explicit close — root's destructor on
// function return will run, but without bus protection;
// in practice the SD layer tolerates this.
return files;
}
}
root.close();
SDAccess::release_bus();
return files;
}
virtual size_t storageSize() override {
if (!SDAccess::acquire_bus(500)) return 0;
size_t s = SD.totalBytes();
SDAccess::release_bus();
return s;
}
virtual size_t storageAvailable() override {
if (!SDAccess::acquire_bus(500)) return 0;
size_t s = SD.totalBytes() - SD.usedBytes();
SDAccess::release_bus();
return s;
}
virtual bool isValid() const override { return SDAccess::is_ready(); }
};
} // namespace _SDArchive
class SDArchiveFileSystem : public microStore::FileSystem {
public:
SDArchiveFileSystem() : microStore::FileSystem(new _SDArchive::FileSystemImpl()) {}
virtual ~SDArchiveFileSystem() {}
};
} // namespace TDeck
} // namespace Hardware
#endif // ARDUINO
#endif // HARDWARE_TDECK_SD_ARCHIVE_FS_H