From 064959db5578d7d2bf51f6ae08a1d42bbbbe7131 Mon Sep 17 00:00:00 2001 From: mverch67 Date: Sat, 24 Oct 2026 15:33:52 +0200 Subject: [PATCH 1/9] refactor file system abstractions into storage and SD bus classes --- AGENTS.md | 2 +- include/filesystem/ArduinoFileSystem.h | 34 - include/filesystem/IFileSystem.h | 87 -- include/filesystem/PosixFileSystem.h | 30 - include/filesystem/SdCard.h | 195 ---- include/filesystem/SdFatFileSystem.h | 34 - include/graphics/map/FileSystemService.h | 23 - include/graphics/map/LinuxFileSystemService.h | 23 - include/graphics/map/MapFileSystem.h | 80 -- include/graphics/map/PMTileService.h | 20 +- include/graphics/map/SDCardService.h | 35 - include/graphics/map/SDMMCCardService.h | 41 - include/graphics/map/SdFatService.h | 35 - include/graphics/map/StorageTileService.h | 20 + include/graphics/view/TFT/TFTView_320x240.h | 8 +- include/storage/FatFsStorage.h | 68 ++ .../RemoteSDService.h => storage/IRemoteFS.h} | 55 +- include/storage/IStorage.h | 108 ++ include/storage/MapStorage.h | 17 + include/storage/PosixStorage.h | 28 + include/storage/RemoteStorage.h | 29 + include/util/ISpiLock.h | 3 +- include/util/SdFatFileWrapper.h | 161 --- library.json | 1 - source/filesystem/ArduinoFileSystem.cpp | 125 --- source/filesystem/FileSystemFactory.cpp | 28 - source/filesystem/PosixFileSystem.cpp | 133 --- source/filesystem/SdCard.cpp | 969 ------------------ source/filesystem/SdFatFileSystem.cpp | 123 --- source/graphics/TFT/TFTView_320x240.cpp | 369 ++----- source/graphics/map/FileSystemService.cpp | 83 -- .../graphics/map/LinuxFileSystemService.cpp | 82 -- source/graphics/map/MapFileSystem.cpp | 186 ---- source/graphics/map/MapTile.cpp | 43 +- source/graphics/map/PMTileService.cpp | 29 +- source/graphics/map/RemoteSDService.cpp | 242 ----- source/graphics/map/SDCardService.cpp | 249 ----- source/graphics/map/SDMMCCardService.cpp | 245 ----- source/graphics/map/SdFatService.cpp | 238 ----- source/graphics/map/StorageTileService.cpp | 82 ++ source/storage/FatFsStorage.cpp | 242 +++++ source/storage/IStorage.cpp | 25 + source/storage/MapStorage.cpp | 89 ++ source/storage/PosixStorage.cpp | 129 +++ source/storage/RemoteStorage.cpp | 220 ++++ source/storage/SdmmcBus.cpp | 112 ++ source/storage/SoftSpiBus.cpp | 411 ++++++++ source/storage/SpiBus.cpp | 91 ++ source/storage/StorageFactory.cpp | 28 + tests/test_Storage.cpp | 242 +++++ 50 files changed, 2090 insertions(+), 3862 deletions(-) delete mode 100644 include/filesystem/ArduinoFileSystem.h delete mode 100644 include/filesystem/IFileSystem.h delete mode 100644 include/filesystem/PosixFileSystem.h delete mode 100644 include/filesystem/SdCard.h delete mode 100644 include/filesystem/SdFatFileSystem.h delete mode 100644 include/graphics/map/FileSystemService.h delete mode 100644 include/graphics/map/LinuxFileSystemService.h delete mode 100644 include/graphics/map/MapFileSystem.h delete mode 100644 include/graphics/map/SDCardService.h delete mode 100644 include/graphics/map/SDMMCCardService.h delete mode 100644 include/graphics/map/SdFatService.h create mode 100644 include/graphics/map/StorageTileService.h create mode 100644 include/storage/FatFsStorage.h rename include/{graphics/map/RemoteSDService.h => storage/IRemoteFS.h} (55%) create mode 100644 include/storage/IStorage.h create mode 100644 include/storage/MapStorage.h create mode 100644 include/storage/PosixStorage.h create mode 100644 include/storage/RemoteStorage.h delete mode 100644 include/util/SdFatFileWrapper.h delete mode 100644 source/filesystem/ArduinoFileSystem.cpp delete mode 100644 source/filesystem/FileSystemFactory.cpp delete mode 100644 source/filesystem/PosixFileSystem.cpp delete mode 100644 source/filesystem/SdCard.cpp delete mode 100644 source/filesystem/SdFatFileSystem.cpp delete mode 100644 source/graphics/map/FileSystemService.cpp delete mode 100644 source/graphics/map/LinuxFileSystemService.cpp delete mode 100644 source/graphics/map/MapFileSystem.cpp delete mode 100644 source/graphics/map/RemoteSDService.cpp delete mode 100644 source/graphics/map/SDCardService.cpp delete mode 100644 source/graphics/map/SDMMCCardService.cpp delete mode 100644 source/graphics/map/SdFatService.cpp create mode 100644 source/graphics/map/StorageTileService.cpp create mode 100644 source/storage/FatFsStorage.cpp create mode 100644 source/storage/IStorage.cpp create mode 100644 source/storage/MapStorage.cpp create mode 100644 source/storage/PosixStorage.cpp create mode 100644 source/storage/RemoteStorage.cpp create mode 100644 source/storage/SdmmcBus.cpp create mode 100644 source/storage/SoftSpiBus.cpp create mode 100644 source/storage/SpiBus.cpp create mode 100644 source/storage/StorageFactory.cpp create mode 100644 tests/test_Storage.cpp diff --git a/AGENTS.md b/AGENTS.md index b81e2d931..d1ec4c6ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ ctest --test-dir build --output-on-failure ## Architecture and Platform Boundaries - C++17 is required (`CMAKE_CXX_STANDARD` is 17 and extensions are disabled). -- Follow existing subsystem boundaries: public contracts generally live under `include/`, with implementations under `source/`. Reuse established interfaces and factories, such as `IClientBase`, `IFileSystem`, `DisplayDriverFactory`, and `ViewFactory`; do not add a parallel abstraction without a concrete need. +- Follow existing subsystem boundaries: public contracts generally live under `include/`, with implementations under `source/`. Reuse established interfaces and factories, such as `IClientBase`, `IStorage`, `DisplayDriverFactory`, and `ViewFactory`; do not add a parallel abstraction without a concrete need. - New shared UI behavior should work across all supported devices. Do not scatter `#ifdef DEVICE_X` checks through shared logic to handle individual boards. - Put device-specific behavior behind the narrowest suitable existing abstraction. When a new behavior genuinely differs by device, prefer an abstract interface with device-specific implementations selected by a factory, following patterns such as `DisplayDriver` and `DisplayDriverFactory`. - Keep device-specific compile-time checks at platform or factory registration boundaries when required by the build or hardware APIs. Follow the existing conditional-registration patterns; do not add an interface or factory when the existing abstraction already handles the variation. diff --git a/include/filesystem/ArduinoFileSystem.h b/include/filesystem/ArduinoFileSystem.h deleted file mode 100644 index 0fc982c16..000000000 --- a/include/filesystem/ArduinoFileSystem.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "IFileSystem.h" - -#if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) || defined(SDCARD_SHARE_SPI) -#include "filesystem/SdCard.h" - -/** - * @brief Arduino File API implementation - * - * Uses Arduino's File API (via SDFs) which provides a common interface - * for ESP32, nRF52, and Portduino platforms. - */ -class ArduinoFileSystem : public IFileSystem -{ - public: - virtual ~ArduinoFileSystem() override = default; - - virtual bool open(const std::string &path, const std::string &mode) override; - virtual void close() override; - virtual bool isOpen() const override; - virtual int printf(const char *format, ...) override; - virtual int write(const void *data, size_t size) override; - virtual int read(void *buffer, size_t size) override; - virtual bool readLine(char *buffer, size_t maxLen) override; - virtual bool mkdir(const std::string &path) override; - virtual std::string getLastError() const override; - - private: - File currentFile; - std::string lastError; -}; - -#endif // ARCH_PORTDUINO || HAS_SD_MMC || SDCARD_SHARE_SPI diff --git a/include/filesystem/IFileSystem.h b/include/filesystem/IFileSystem.h deleted file mode 100644 index 3b3fcd4c1..000000000 --- a/include/filesystem/IFileSystem.h +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once - -#include -#include -#include - -/** - * @brief Abstract file system interface to handle platform-specific file I/O - * - * Provides a unified abstraction over different file system implementations: - * - P4 POSIX (IDF VFS mounted at /sdcard) - * - Arduino File API (ESP32-S3, nRF52, etc.) - * - SdFat library interface - */ -class IFileSystem -{ - public: - virtual ~IFileSystem() = default; - - /** - * @brief Open a file - * @param path File path - * @param mode "r" for read, "w" for write, "a" for append - * @return true if successful, false otherwise - */ - virtual bool open(const std::string &path, const std::string &mode) = 0; - - /** - * @brief Close current file - */ - virtual void close() = 0; - - /** - * @brief Check if file is open - * @return true if a file is currently open - */ - virtual bool isOpen() const = 0; - - /** - * @brief Write formatted string to file (like fprintf) - * @return number of characters written, or -1 on error - */ - virtual int printf(const char *format, ...) = 0; - - /** - * @brief Write raw bytes to file - * @param data Pointer to data - * @param size Number of bytes to write - * @return number of bytes written, or -1 on error - */ - virtual int write(const void *data, size_t size) = 0; - - /** - * @brief Read bytes from file - * @param buffer Buffer to store data - * @param size Maximum bytes to read - * @return number of bytes read, or -1 on error - */ - virtual int read(void *buffer, size_t size) = 0; - - /** - * @brief Read a line from file (up to newline) - * @param buffer Buffer to store line - * @param maxLen Maximum length including null terminator - * @return true if line was read, false on EOF or error - */ - virtual bool readLine(char *buffer, size_t maxLen) = 0; - - /** - * @brief Create directory recursively - * @param path Directory path - * @return true if successful or already exists, false on error - */ - virtual bool mkdir(const std::string &path) = 0; - - /** - * @brief Get last error message - * @return Error string describing last operation failure - */ - virtual std::string getLastError() const = 0; -}; - -/** - * @brief Get platform-appropriate file system implementation - * @return Unique pointer to IFileSystem instance - */ -std::unique_ptr createFileSystem(); diff --git a/include/filesystem/PosixFileSystem.h b/include/filesystem/PosixFileSystem.h deleted file mode 100644 index e9a5198d9..000000000 --- a/include/filesystem/PosixFileSystem.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "IFileSystem.h" -#include - -/** - * @brief POSIX file system implementation for ESP32-P4 - * - * Uses POSIX file APIs (fopen, fprintf, fgets, etc.) which work with - * ESP-IDF's VFS-mounted file systems at /sdcard. - */ -class PosixFileSystem : public IFileSystem -{ - public: - virtual ~PosixFileSystem() override; - - virtual bool open(const std::string &path, const std::string &mode) override; - virtual void close() override; - virtual bool isOpen() const override; - virtual int printf(const char *format, ...) override; - virtual int write(const void *data, size_t size) override; - virtual int read(void *buffer, size_t size) override; - virtual bool readLine(char *buffer, size_t maxLen) override; - virtual bool mkdir(const std::string &path) override; - virtual std::string getLastError() const override; - - private: - FILE *currentFile = nullptr; - std::string lastError; -}; diff --git a/include/filesystem/SdCard.h b/include/filesystem/SdCard.h deleted file mode 100644 index 004ad4ee4..000000000 --- a/include/filesystem/SdCard.h +++ /dev/null @@ -1,195 +0,0 @@ -#pragma once - -#define FS_NO_GLOBALS - -#if defined(ARCH_PORTDUINO) -#include "PortduinoFS.h" -extern fs::FS &SDFs; - -#elif defined(HAS_SD_MMC) -#include "SD_MMC.h" // TODO: replace by SdFat SDIO -extern fs::SDMMCFS &SDFs; -using fs::File; -#elif defined(SDCARD_SHARE_SPI) // share SPI instance with main project -#include "SD.h" -#include -extern fs::SDFS &SDFs; -#elif defined(HAS_SDCARD) -#include "SdFat.h" -extern SdFs SDFs; -#endif - -#include -#include -#include - -/** - * SdCard adapter to define a common interface for SD and SdFs - */ -class ISdCard -{ - public: - enum CardType { - eNone, - eMMC, - eSD, - eSDHC, - eSDXC, - eSDUC, - eUnknown, - }; - - enum FatType { - eNA, - eFat16, - eFat32, - eExFat, - }; - - enum ErrorType { eNoError, eSlotEmpty, eCardError, eFormatError, eNoMbrError, eUnknownError }; - - virtual bool init(void) = 0; - virtual CardType cardType(void) = 0; - virtual FatType fatType(void) = 0; - virtual ErrorType errorType(void) = 0; - - virtual uint64_t usedBytes(void) = 0; - virtual uint64_t freeBytes(void) = 0; - // false while used/free are not known yet (e.g. a background scan on a - // co-processor has not finished); local cards always know them - virtual bool statsValid(void) { return true; } - - enum StatsResult { - eStatsValid, // used/free are up to date - eStatsPending, // still being computed, ask again later - eStatsUnavailable, // card or link gone, stop asking - }; - // Re-read used/free without re-detecting the card - virtual StatsResult refreshStats(void) { return eStatsValid; } - // Release the card so it can be pulled safely. False when the card cannot - // be ejected (locally mounted cards are simply not held open). - virtual bool eject(void) { return false; } - virtual uint64_t cardSize(void) = 0; - virtual bool format(void) = 0; - - bool isUpdated(void) { return updated; } - virtual std::set loadMapStyles(const char *folder) = 0; - virtual bool hasMapArchive(const char *folder, const char *style) = 0; - virtual std::string getUrlProvider(const char *folder, const char *style) = 0; - virtual bool setUrlProvider(const char *folder, const char *style, const char *urlTemplate) = 0; - virtual ~ISdCard(void) {} - - protected: - bool updated = false; -}; - -// SENSECAP_INDICATOR takes precedence: the SD card sits behind the -// co-processor even when a generic SD define is also set -#if (defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) || defined(SDCARD_SHARE_SPI)) && !defined(SENSECAP_INDICATOR) -class SDCard : public ISdCard -{ - public: - bool init(void) override; - CardType cardType(void) override; - FatType fatType(void) override; - ErrorType errorType(void) override; - uint64_t usedBytes(void) override; - uint64_t freeBytes(void) override; - uint64_t cardSize(void) override; - bool format(void) override { return false; }; - - std::set loadMapStyles(const char *folder) override; - bool hasMapArchive(const char *folder, const char *style) override; - std::string getUrlProvider(const char *folder, const char *style) override; - bool setUrlProvider(const char *folder, const char *style, const char *urlTemplate) override; - virtual ~SDCard(void); -}; - -#elif defined(HAS_SDCARD) && !defined(SENSECAP_INDICATOR) -class SdFsCard : public ISdCard -{ - public: - bool init(void) override; - CardType cardType(void) override; - FatType fatType(void) override; - ErrorType errorType(void) override; - uint64_t usedBytes(void) override; - uint64_t freeBytes(void) override; - uint64_t cardSize(void) override; - bool format(void) override; - - std::set loadMapStyles(const char *folder) override; - bool hasMapArchive(const char *folder, const char *style) override; - std::string getUrlProvider(const char *folder, const char *style) override; - bool setUrlProvider(const char *folder, const char *style, const char *urlTemplate) override; - virtual ~SdFsCard(void) {} -}; - -#elif defined(SENSECAP_INDICATOR) -#include "graphics/map/RemoteSDService.h" - -/** - * SD card behind a co-processor, accessed through the RemoteSDService - * IRemoteFS backend. Statistics are fetched once over the link on init(). - */ -class RemoteSdCard : public ISdCard -{ - public: - bool init(void) override; - CardType cardType(void) override; - FatType fatType(void) override; - ErrorType errorType(void) override - { - if (info.present) - return eNoError; - // a card that is in there but has no filesystem can be formatted, - // just like a local one - return info.unformatted ? eFormatError : eSlotEmpty; - } - uint64_t usedBytes(void) override { return info.usedBytes; } - uint64_t freeBytes(void) override { return info.freeBytes; } - bool statsValid(void) override { return info.statsValid; } - StatsResult refreshStats(void) override; - bool eject(void) override; - uint64_t cardSize(void) override { return info.cardSize; } - bool format(void) override; - - std::set loadMapStyles(const char *folder) override; - bool hasMapArchive(const char *folder, const char *style) override; - std::string getUrlProvider(const char *folder, const char *style) override; - bool setUrlProvider(const char *folder, const char *style, const char *urlTemplate) override; - virtual ~RemoteSdCard(void) {} - - private: - RemoteSdInfo info; -}; - -#endif - -/** - * Dummy to emulate sdCard interface without card reader or without card inserted - */ -class NoSdCard : public ISdCard -{ - public: - bool init(void) override { return true; } - CardType cardType(void) override { return CardType::eNone; } - FatType fatType(void) override { return FatType::eNA; } - ErrorType errorType(void) override { return ErrorType::eNoError; } - uint64_t usedBytes(void) override { return 0; } - uint64_t freeBytes(void) override { return 0; } - uint64_t cardSize(void) override { return 1; } - bool format(void) override { return false; } - - std::set loadMapStyles(const char *folder) override - { - updated = true; - return std::set{}; - } - bool hasMapArchive(const char *folder, const char *style) override { return false; } - std::string getUrlProvider(const char *folder, const char *style) override { return {}; } - bool setUrlProvider(const char *folder, const char *style, const char *urlTemplate) override { return false; } - virtual ~NoSdCard(void) {} -}; - -extern ISdCard *sdCard; \ No newline at end of file diff --git a/include/filesystem/SdFatFileSystem.h b/include/filesystem/SdFatFileSystem.h deleted file mode 100644 index 0e7947f70..000000000 --- a/include/filesystem/SdFatFileSystem.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "IFileSystem.h" - -#if defined(HAS_SDCARD) && !defined(HAS_SD_MMC) && !defined(SDCARD_SHARE_SPI) -#include "filesystem/SdCard.h" - -/** - * @brief SdFat library implementation - * - * Uses the SdFat library's FsFile interface, typically used on - * platforms with dedicated SD card SPI connections. - */ -class SdFatFileSystem : public IFileSystem -{ - public: - virtual ~SdFatFileSystem() override = default; - - virtual bool open(const std::string &path, const std::string &mode) override; - virtual void close() override; - virtual bool isOpen() const override; - virtual int printf(const char *format, ...) override; - virtual int write(const void *data, size_t size) override; - virtual int read(void *buffer, size_t size) override; - virtual bool readLine(char *buffer, size_t maxLen) override; - virtual bool mkdir(const std::string &path) override; - virtual std::string getLastError() const override; - - private: - FsFile currentFile; - std::string lastError; -}; - -#endif // HAS_SDCARD diff --git a/include/graphics/map/FileSystemService.h b/include/graphics/map/FileSystemService.h deleted file mode 100644 index 2569ae96c..000000000 --- a/include/graphics/map/FileSystemService.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "graphics/map/TileService.h" -#include "lvgl.h" - -class FileSystemService : public ITileService -{ - public: - FileSystemService(); - - virtual ~FileSystemService(); - - bool load(const char *name, void *img) override; - - protected: - static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); - static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); - static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); - static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); - static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); - static lv_fs_res_t fs_size(lv_fs_drv_t *drv, void *file_p, uint32_t *size_p); - static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); -}; diff --git a/include/graphics/map/LinuxFileSystemService.h b/include/graphics/map/LinuxFileSystemService.h deleted file mode 100644 index 5efd49ea5..000000000 --- a/include/graphics/map/LinuxFileSystemService.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "graphics/map/TileService.h" -#include "lvgl.h" - -class LinuxFileSystemService : public ITileService -{ - public: - LinuxFileSystemService(); - - virtual ~LinuxFileSystemService(); - - bool load(const char *name, void *img) override; - - protected: - static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); - static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); - static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); - static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); - static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); - static lv_fs_res_t fs_size(lv_fs_drv_t *drv, void *file_p, uint32_t *size_p); - static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); -}; diff --git a/include/graphics/map/MapFileSystem.h b/include/graphics/map/MapFileSystem.h deleted file mode 100644 index eb48ab650..000000000 --- a/include/graphics/map/MapFileSystem.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include -#if defined(SDCARD_SHARE_SPI) && !defined(ARCH_PORTDUINO) && !defined(HAS_SD_MMC) -#include "SD.h" -#endif - -/** - * Abstraction class helper to open, close and read files from different file systems - */ -class IMapFileSystem -{ - public: - virtual bool open(const char *path) = 0; - virtual void close(void) = 0; - virtual bool readAt(uint64_t offset, uint8_t *buf, uint32_t len) = 0; - virtual ~IMapFileSystem() {} -}; - -#if (defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) || defined(SDCARD_SHARE_SPI)) && !defined(SENSECAP_INDICATOR) - -#if (defined(SDCARD_SHARE_SPI) || defined(HAS_SD_MMC)) && !defined(ARCH_PORTDUINO) -#include "FS.h" -#endif -#include "filesystem/SdCard.h" - -// On SDIO the archive is read through FatFs instead of the VFS: fs::File::seek() -// takes a uint32_t and ends in fseek(long), so exFAT offsets past 2 GiB fail. -#if defined(HAS_SD_MMC) && !defined(ARCH_PORTDUINO) -#define MAPFILE_USE_FATFS 1 -#endif - -class SDMapFileSystem : public IMapFileSystem -{ - public: - bool open(const char *path) override; - void close(void) override; - bool readAt(uint64_t offset, uint8_t *buf, uint32_t len) override; -#ifdef MAPFILE_USE_FATFS - ~SDMapFileSystem(void) override { close(); } -#endif - - private: -#ifdef MAPFILE_USE_FATFS - void *fil = nullptr; // FatFs FIL, kept opaque so ff.h stays out of this header -#else - File file; -#endif -}; - -#elif defined(HAS_SDCARD) && !defined(SENSECAP_INDICATOR) - -#include "filesystem/SdCard.h" - -class SdFatMapFileSystem : public IMapFileSystem -{ - public: - bool open(const char *path) override; - void close(void) override; - bool readAt(uint64_t offset, uint8_t *buf, uint32_t len) override; - - private: - FsFile file; -}; - -#elif defined(SENSECAP_INDICATOR) - -class RemoteMapFileSystem : public IMapFileSystem -{ - public: - RemoteMapFileSystem(); - bool open(const char *path) override; - void close(void) override; - bool readAt(uint64_t offset, uint8_t *buf, uint32_t len) override; - - private: - char path[256]; -}; - -#endif \ No newline at end of file diff --git a/include/graphics/map/PMTileService.h b/include/graphics/map/PMTileService.h index 7950176b2..444ed889e 100644 --- a/include/graphics/map/PMTileService.h +++ b/include/graphics/map/PMTileService.h @@ -1,23 +1,21 @@ #pragma once -#include "graphics/map/MapFileSystem.h" #include "graphics/map/MapTileSettings.h" #include "graphics/map/TileService.h" #include "graphics/map/pmtiles.hpp" #include "lvgl.h" - -#if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) || defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) +#include "storage/IStorage.h" /** - * Serves raster tiles out of a single .pmtiles archive on the SD card. + * Serves raster tiles out of a single .pmtiles archive on the storage. * Tiles the archive does not contain are handed to the fallback service - * (plain z/x/y files on SD), which is also where fetched tiles are saved. + * (plain z/x/y files), which is also where fetched tiles are saved. */ class PMTileService : public ITileService { public: - // takes ownership of fallback and archiveFS - explicit PMTileService(ITileService *fallback = nullptr, IMapFileSystem *archiveFS = nullptr); + // takes ownership of fallback + explicit PMTileService(IStorage &storage, ITileService *fallback = nullptr); virtual ~PMTileService(); @@ -25,7 +23,7 @@ class PMTileService : public ITileService bool save(const char *name, void *img, size_t len) override; protected: - // the archive tracks the selected style; reopens when it changes + // the archive tracks the selected style and the storage mount; reopens when either changes bool openArchive(void); void closeArchive(void); bool loadFromArchive(uint32_t z, uint32_t x, uint32_t y, void *img); @@ -34,9 +32,11 @@ class PMTileService : public ITileService private: static constexpr uint8_t MAX_DIR_DEPTH = 4; + IStorage &storage; ITileService *fallback; - IMapFileSystem *archiveFS; + std::unique_ptr archive; char openedStyle[MapTileSettings::TILE_STYLE_SIZE]; + uint32_t openedGeneration; bool archiveValid; pmtiles::headerv3 pmHeader; @@ -45,5 +45,3 @@ class PMTileService : public ITileService uint64_t cachedDirOffset[MAX_DIR_DEPTH]; pmtiles::directory cachedDirEntries[MAX_DIR_DEPTH]; }; - -#endif diff --git a/include/graphics/map/SDCardService.h b/include/graphics/map/SDCardService.h deleted file mode 100644 index 2ce7b800e..000000000 --- a/include/graphics/map/SDCardService.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "FS.h" -#include "graphics/map/TileService.h" -#include "lvgl.h" -#include -#include - -class SDCardService : public ITileService -{ - public: - SDCardService(); - - virtual ~SDCardService(); - - bool load(const char *name, void *img) override; - bool save(const char *name, void *img, size_t len) override; - - protected: - static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); - static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); - static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); - static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); - static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); - static lv_fs_res_t fs_size(lv_fs_drv_t *drv, void *file_p, uint32_t *size_p); - static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); - static void *fs_dir_open(lv_fs_drv_t *drv, const char *path); - static lv_fs_res_t fs_dir_read(lv_fs_drv_t *drv, void *rddir_p, char *fn, uint32_t fn_len); - static lv_fs_res_t fs_dir_close(lv_fs_drv_t *drv, void *rddir_p); - - private: - typedef struct SdFile { - File file; - } SdFile; -}; diff --git a/include/graphics/map/SDMMCCardService.h b/include/graphics/map/SDMMCCardService.h deleted file mode 100644 index e2366f977..000000000 --- a/include/graphics/map/SDMMCCardService.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "Arduino.h" -#include "graphics/map/TileService.h" -#include "lvgl.h" -#include - -#if defined(HAS_SD_MMC) && defined(CONFIG_IDF_TARGET_ESP32P4) - -/** - * special SD card class for esp32-p4 + MMC SD - */ - -class SDMMCCardService : public ITileService -{ - public: - SDMMCCardService(); - virtual ~SDMMCCardService(); - - bool load(const char *name, void *img) override; - bool save(const char *name, void *img, size_t len) override; - - protected: - static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); - static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); - static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); - static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); - static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); - static lv_fs_res_t fs_size(lv_fs_drv_t *drv, void *file_p, uint32_t *size_p); - static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); - static void *fs_dir_open(lv_fs_drv_t *drv, const char *path); - static lv_fs_res_t fs_dir_read(lv_fs_drv_t *drv, void *rddir_p, char *fn, uint32_t fn_len); - static lv_fs_res_t fs_dir_close(lv_fs_drv_t *drv, void *rddir_p); - - private: - typedef struct SdFile { - FILE *file; - } SdFile; -}; - -#endif diff --git a/include/graphics/map/SdFatService.h b/include/graphics/map/SdFatService.h deleted file mode 100644 index fc7c271f7..000000000 --- a/include/graphics/map/SdFatService.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "SdFat.h" -#include "graphics/map/TileService.h" -#include "lvgl.h" -#include -#include - -class SdFatService : public ITileService -{ - public: - SdFatService(); - - virtual ~SdFatService(); - - bool load(const char *name, void *img) override; - bool save(const char *name, void *img, size_t len) override; - - protected: - static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); - static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); - static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); - static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); - static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); - static lv_fs_res_t fs_size(lv_fs_drv_t *drv, void *file_p, uint32_t *size_p); - static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); - static void *fs_dir_open(lv_fs_drv_t *drv, const char *path); - static lv_fs_res_t fs_dir_read(lv_fs_drv_t *drv, void *rddir_p, char *fn, uint32_t fn_len); - static lv_fs_res_t fs_dir_close(lv_fs_drv_t *drv, void *rddir_p); - - private: - typedef struct SdFile { - FsFile file; - } SdFile; -}; diff --git a/include/graphics/map/StorageTileService.h b/include/graphics/map/StorageTileService.h new file mode 100644 index 000000000..d2c48f2c5 --- /dev/null +++ b/include/graphics/map/StorageTileService.h @@ -0,0 +1,20 @@ +#pragma once + +#include "graphics/map/TileService.h" + +class IStorage; + +/** + * Loads z/x/y tiles from, and saves fetched tiles to, any storage volume. + */ +class StorageTileService : public ITileService +{ + public: + explicit StorageTileService(IStorage &storage); + + bool load(const char *name, void *img) override; + bool save(const char *name, void *img, size_t len) override; + + private: + IStorage &storage; +}; diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index 0a289b998..992cd83a8 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -13,6 +13,8 @@ class MapPanel; class TFTView_Debug; +class IStorage; +struct StorageInfo; /** * @brief RAII helper to temporarily change the default LVGL input group @@ -255,9 +257,7 @@ class TFTView_320x240 : public MeshtasticView, private NodeListActionSink void armSDCardStatsPoll(void); // release the card so it can be pulled safely; a tap mounts it again void ejectSDCard(void); -#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) - void formatSDCardLabel(char *buf, size_t len); -#endif + void formatSDCardLabel(const StorageInfo &info, char *buf, size_t len); uint16_t sdStatsPolls = 0; // format SD card if invalid virtual void formatSDCard(void); @@ -543,6 +543,8 @@ class TFTView_320x240 : public MeshtasticView, private NodeListActionSink bool detectorRunning; // meshDetector is active bool cardDetected; // SD has been detected bool formatSD; // offer to format SD card + IStorage *storage = nullptr; // SD card (or host folder) + bool mapStylesScanned = false; // map styles read from the current mount uint16_t buttonSize; // size of group/chat buttons in pixels uint16_t statisticTableRows; // number of rows in statistics table uint16_t packetCounter; // number of packets in packet log diff --git a/include/storage/FatFsStorage.h b/include/storage/FatFsStorage.h new file mode 100644 index 000000000..e96d7df2b --- /dev/null +++ b/include/storage/FatFsStorage.h @@ -0,0 +1,68 @@ +#pragma once + +#include "storage/IStorage.h" +#include +#include +#include +#include + +/** + * How a card is attached (SDMMC, SPI, bit-banged SPI). A bus brings the card up, + * mounts its FAT volume into FatFs and VFS, and tears it down again. + */ +class ISdBus +{ + public: + virtual ~ISdBus() = default; + // formatIfNeeded puts a fresh filesystem on a card that has none + virtual IStorage::Status mount(bool formatIfNeeded) = 0; + virtual void unmount(void) = 0; + // FatFs physical drive of the mounted volume + virtual uint8_t drive(void) const = 0; + virtual StorageInfo::CardType cardType(void) = 0; + virtual fs::FS *arduinoFS(void) = 0; +}; + +// one per card interface, defined only on boards that have it +std::unique_ptr createSdmmcBus(void); +std::unique_ptr createSpiBus(void); +std::unique_ptr createSoftSpiBus(void); + +class FatFsFile; + +/** + * SD card on ESP32, accessed through the FatFs API rather than VFS: VFS offsets are a + * 32-bit off_t, FatFs with exFAT seeks with 64 bit. + */ +class FatFsStorage : public IStorage +{ + public: + explicit FatFsStorage(std::unique_ptr bus); + ~FatFsStorage() override; + + Status mount(void) override; + bool eject(void) override; + bool format(void) override; + bool info(StorageInfo &out) override; + + std::unique_ptr open(const char *path, OpenMode mode) override; + bool exists(const char *path) override; + bool mkdir(const char *path) override; + bool remove(const char *path) override; + bool list(const char *dir, const std::function &cb) override; + + fs::FS *arduinoFS(void) override; + + private: + friend class FatFsFile; + + std::string fatPath(const char *path) const; + // closes all open files and unmounts + void release(void); + + std::unique_ptr bus; + bool mounted = false; + // guards the open file list and every file operation against release() + std::mutex ioLock; + std::vector files; +}; diff --git a/include/graphics/map/RemoteSDService.h b/include/storage/IRemoteFS.h similarity index 55% rename from include/graphics/map/RemoteSDService.h rename to include/storage/IRemoteFS.h index e7b6c1aa4..f444adff6 100644 --- a/include/graphics/map/RemoteSDService.h +++ b/include/storage/IRemoteFS.h @@ -1,7 +1,5 @@ #pragma once -#include "graphics/map/TileService.h" -#include "lvgl.h" #include #include #include @@ -25,10 +23,9 @@ struct RemoteSdInfo { }; /** - * Backend transport for RemoteSDService: chunked file access on a filesystem - * that lives on another MCU (e.g. the SD card behind the SenseCAP Indicator - * RP2040). Implemented and registered by the firmware before the view loads - * the map. + * Transport for RemoteStorage: chunked file access on a filesystem that lives + * on another MCU (e.g. the SD card behind the SenseCAP Indicator RP2040). + * Implemented and registered by the firmware before the view starts. */ class IRemoteFS { @@ -75,49 +72,3 @@ class IRemoteFS virtual bool sdFormat(void) = 0; virtual ~IRemoteFS() {} }; - -/** - * Map tile service for a remote SD card, accessed chunk-wise through an - * IRemoteFS backend. Registers an LVGL filesystem driver so the image - * decoder reads tiles like from a local drive. - */ -class RemoteSDService : public ITileService -{ - public: - static void setBackend(IRemoteFS *fs); - static IRemoteFS *backend(void); - - RemoteSDService(); - virtual ~RemoteSDService(); - - bool load(const char *name, void *img) override; - bool save(const char *name, void *img, size_t len) override; - - protected: - static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); - static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); - static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); - static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); - static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); - static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); - - private: - static IRemoteFS *remoteFS; - - enum { CHUNK_SIZE = 4096 }; // matches FileTransfer.filedata max_size - - typedef struct RemoteFile { - char path[256]; // full FAT LFN paths round-trip - uint32_t pos; - uint32_t size; - } RemoteFile; - - // One chunk for all open files: the image decoder opens the same tile - // three times and reads it front to back, so they all want the same bytes. - static uint8_t cachedChunk[CHUNK_SIZE]; - static char cachedPath[256]; - static uint32_t cachedOffset; - static uint32_t cachedLen; - static uint32_t cachedSize; - static uint32_t chunkAt(const char *path, uint32_t pos, uint32_t *fileSize); -}; diff --git a/include/storage/IStorage.h b/include/storage/IStorage.h new file mode 100644 index 000000000..3bafbeff2 --- /dev/null +++ b/include/storage/IStorage.h @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include + +namespace fs +{ +class FS; +} + +enum class OpenMode { + Read, + Write, // truncates + Append, +}; + +/** + * An open file, closed by the destructor. Offsets are 64 bit so that exFAT + * archives beyond 4 GiB can be addressed. + */ +class IFile +{ + public: + virtual ~IFile() = default; + + // bytes transferred, -1 on error + virtual int32_t read(void *buf, uint32_t len) = 0; + virtual int32_t write(const void *buf, uint32_t len) = 0; + virtual bool seek(uint64_t pos) = 0; + virtual uint64_t size(void) = 0; + + bool readAt(uint64_t pos, void *buf, uint32_t len) { return seek(pos) && read(buf, len) == (int32_t)len; } + // reads up to (excluding) the next '\n'; false at end of file + bool readLine(char *buf, uint32_t len); + bool print(const char *text); +}; + +struct StorageInfo { + enum CardType : uint8_t { eNone, eMMC, eSD, eSDHC, eSDXC, eUnknown }; + enum FatType : uint8_t { eNA, eFat16, eFat32, eExFat }; + + CardType card = eNone; + FatType fat = eNA; + uint64_t size = 0; + uint64_t used = 0; + uint64_t free = 0; + // false while used/free are still being computed (e.g. by a co-processor) + bool statsValid = true; +}; + +/** + * A storage volume (SD card or a host folder) with the file operations the UI needs. + * Paths are absolute ("/maps/..."), relative to the root of the volume. + */ +class IStorage +{ + public: + enum class Status { Ok, SlotEmpty, CardError, NoFilesystem }; + + virtual ~IStorage() = default; + + // false when there is no removable card to show in the UI + virtual bool hasSlot(void) const { return true; } + // (re-)detects and mounts the card + virtual Status mount(void) = 0; + // releases the card so it can be pulled safely + virtual bool eject(void) { return false; } + // puts a fresh filesystem on a card that has none + virtual bool format(void) { return false; } + // false when the card or the link to it is gone + virtual bool info(StorageInfo &out) = 0; + + virtual std::unique_ptr open(const char *path, OpenMode mode) = 0; + virtual bool exists(const char *path) = 0; + // creates missing parent directories as well + virtual bool mkdir(const char *path) = 0; + virtual bool remove(const char *path) = 0; + virtual bool list(const char *dir, const std::function &cb) = 0; + + // Arduino view of the volume (FTP server); nullptr when not available + virtual fs::FS *arduinoFS(void) { return nullptr; } + + // changes whenever the volume is (re-)mounted or released; open files do not survive that + uint32_t generation(void) const { return gen; } + + protected: + uint32_t gen = 0; +}; + +/** + * Storage without a card: every operation fails. + */ +class NullStorage : public IStorage +{ + public: + bool hasSlot(void) const override { return false; } + Status mount(void) override { return Status::SlotEmpty; } + bool info(StorageInfo &out) override { return false; } + std::unique_ptr open(const char *, OpenMode) override { return nullptr; } + bool exists(const char *) override { return false; } + bool mkdir(const char *) override { return false; } + bool remove(const char *) override { return false; } + bool list(const char *, const std::function &) override { return false; } +}; + +// the storage backend of this platform; the only place that selects one +std::unique_ptr createStorage(void); diff --git a/include/storage/MapStorage.h b/include/storage/MapStorage.h new file mode 100644 index 000000000..b49a77351 --- /dev/null +++ b/include/storage/MapStorage.h @@ -0,0 +1,17 @@ +#pragma once + +#include "storage/IStorage.h" +#include +#include + +/** + * Map tile layout on a storage volume: /